From 05e8f76c63961d5c6929b48dfdc724fd50d202bf Mon Sep 17 00:00:00 2001 From: pedrule Date: Thu, 30 Jun 2022 00:34:04 +0200 Subject: [PATCH 1/5] update to create local dev env --- .dockerignore | 2 ++ .env | 2 ++ .vscode/launch.json | 15 ++++++++++++ Dockerfile | 16 ++++++++++++ docker-compose.yml | 60 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 95 insertions(+) create mode 100644 .env create mode 100644 .vscode/launch.json create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore index 4d913fbbc91..649aa99fe29 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,3 +14,5 @@ dist test/e2e test/mock-* cypress +docker-compose +Dockerfile diff --git a/.env b/.env new file mode 100644 index 00000000000..2b283517356 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +PWD=/Users/macbookpro/dev/shadow +SECRETS_SYSTEM=RoRuGCWdmIBL7wYojqjUY542DLKvQOIx \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000000..3651d900f92 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Utilisez IntelliSense pour en savoir plus sur les attributs possibles. + // Pointez pour afficher la description des attributs existants. + // Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}" + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000..bdbae5e036d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM docker:dind as docker +FROM golang:1.17.11-buster +# FROM --platform=linux/amd64 golang:1.17.11-buster +RUN apt-get update && apt-get install -y git gcc bash make curl docker-compose +COPY --from=docker /usr/local/bin/docker /usr/local/bin/docker +RUN curl -sL https://deb.nodesource.com/setup_16.x | bash && apt-get install -y nodejs +# COPY /Users/macbookpro/dev/shadow/hydra_ory /Users/macbookpro/dev/shadow/hydra +# WORKDIR /Users/macbookpro/dev/shadow/hydra +# RUN apt-get update && apt-get install -y pass gnupg2 +WORKDIR /usr +RUN git clone https://github.com/go-delve/delve +WORKDIR /usr/delve +RUN go install github.com/go-delve/delve/cmd/dlv + +# ENTRYPOINT [ "gpg2", "–", "gen-key", "&&", "pass", "init", '\$gpg_id' ] + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000000..d3df163dd20 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,60 @@ +version: "3.7" +services: + hydra-build-go: + container_name: hydra-build-go + depends_on: + - init-dev + image: oryd/hydra:latest-sqlite + command: serve all --dangerous-force-http + # command: serve -c /etc/config/hydra/hydra.yml all --dangerous-force-http + environment: + - DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true + - SECRETS_SYSTEM=$SECRETS_SYSTEM + volumes: + - type: volume + source: hydra-sqlite + target: /var/lib/sqlite + # - type: bind + # source: ./config_hydra.yml + # target: /etc/config/hydra/hydra.yml + restart: unless-stopped + networks: + intranet: + oathkeeper: + # entrypoint: /bin/sh -c "while sleep 1000; do :; done;" + hydra-migrate: + image: oryd/hydra:latest-sqlite + environment: + - DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true + command: migrate sql -e --yes + # command: migrate -c /etc/config/hydra/hydra.yml sql -e --yes + volumes: + - type: volume + source: hydra-sqlite + target: /var/lib/sqlite + read_only: false + # - type: bind + # source: ./config_hydra.yml + # target: /etc/config/hydra/hydra.yml + restart: on-failure + networks: + intranet: + init-dev: + image: go-build + container_name: go-builder + build: . + entrypoint: /bin/bash -c "while sleep 1000; do :; done;" + volumes: + - type: bind + source: /var/run/docker.sock + target: /var/run/docker.sock + - type: bind + source: ${PWD} + target: $PWD + +networks: + intranet: + oathkeeper: + external: true +volumes: + hydra-sqlite: \ No newline at end of file From 70ad28d553f3ea8be78389fd4f59474c9de8dbbf Mon Sep 17 00:00:00 2001 From: pedrule Date: Mon, 18 Jul 2022 14:41:58 +0200 Subject: [PATCH 2/5] Merge remote-tracking branch 'ory/master' --- .github/ISSUE_TEMPLATE/BUG-REPORT.yml | 50 ++-- .github/ISSUE_TEMPLATE/DESIGN-DOC.yml | 32 +-- .github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml | 30 +-- .github/config.yml | 2 +- .github/pull_request_template.md | 6 +- .github/semantic.yml | 17 -- .github/workflows/closed_references.yml | 6 +- .github/workflows/conventional_commits.yml | 75 ++++++ .github/workflows/milestone.yml | 6 +- .github/workflows/stale.yml | 10 +- CODE_OF_CONDUCT.md | 141 +++++++++--- CONTRIBUTING.md | 256 +++++++++++++-------- README.md | 77 ++++--- SECURITY.md | 11 +- client/handler.go | 3 +- cmd/token_user.go | 2 +- quickstart-tracing.yml | 2 +- x/json.go | 45 ---- x/json_test.go | 116 ---------- x/oauth2cors/cors.go | 6 + x/oauth2cors/cors_test.go | 18 +- 21 files changed, 491 insertions(+), 420 deletions(-) delete mode 100644 .github/semantic.yml create mode 100644 .github/workflows/conventional_commits.yml delete mode 100644 x/json.go delete mode 100644 x/json_test.go diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml index 02c1e6b5e55..0e152f537ac 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -1,17 +1,17 @@ -description: 'Create a bug report' +description: "Create a bug report" labels: - bug -name: 'Bug Report' +name: "Bug Report" body: - attributes: value: "Thank you for taking the time to fill out this bug report!\n" type: markdown - attributes: - label: 'Preflight checklist' + label: "Preflight checklist" options: - label: - 'I could not find a solution in the existing issues, docs, nor - discussions.' + "I could not find a solution in the existing issues, docs, nor + discussions." required: true - label: "I agree to follow this project's [Code of @@ -22,18 +22,18 @@ body: Guidelines](https://github.com/ory/hydra/blob/master/CONTRIBUTING.md)." required: true - label: - 'This issue affects my [Ory Cloud](https://www.ory.sh/) project.' + "This issue affects my [Ory Cloud](https://www.ory.sh/) project." - label: - 'I have joined the [Ory Community Slack](https://slack.ory.sh).' + "I have joined the [Ory Community Slack](https://slack.ory.sh)." - label: - 'I am signed up to the [Ory Security Patch - Newsletter](https://ory.us10.list-manage.com/subscribe?u=ffb1a878e4ec6c0ed312a3480&id=f605a41b53).' + "I am signed up to the [Ory Security Patch + Newsletter](https://ory.us10.list-manage.com/subscribe?u=ffb1a878e4ec6c0ed312a3480&id=f605a41b53)." id: checklist type: checkboxes - attributes: - description: 'A clear and concise description of what the bug is.' - label: 'Describe the bug' - placeholder: 'Tell us what you see!' + description: "A clear and concise description of what the bug is." + label: "Describe the bug" + placeholder: "Tell us what you see!" id: describe-bug type: textarea validations: @@ -47,17 +47,17 @@ body: 1. Run `docker run ....` 2. Make API Request to with `curl ...` 3. Request fails with response: `{"some": "error"}` - label: 'Reproducing the bug' + label: "Reproducing the bug" id: reproduce-bug type: textarea validations: required: true - attributes: description: - 'Please copy and paste any relevant log output. This will be + "Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. Please - redact any sensitive information' - label: 'Relevant log output' + redact any sensitive information" + label: "Relevant log output" render: shell placeholder: | log=error .... @@ -65,10 +65,10 @@ body: type: textarea - attributes: description: - 'Please copy and paste any relevant configuration. This will be + "Please copy and paste any relevant configuration. This will be automatically formatted into code, so no need for backticks. Please - redact any sensitive information!' - label: 'Relevant configuration' + redact any sensitive information!" + label: "Relevant configuration" render: yml placeholder: | server: @@ -77,14 +77,14 @@ body: id: config type: textarea - attributes: - description: 'What version of our software are you running?' + description: "What version of our software are you running?" label: Version id: version type: input validations: required: true - attributes: - label: 'On which operating system are you observing this issue?' + label: "On which operating system are you observing this issue?" options: - Ory Cloud - macOS @@ -95,19 +95,19 @@ body: id: operating-system type: dropdown - attributes: - label: 'In which environment are you deploying?' + label: "In which environment are you deploying?" options: - Ory Cloud - Docker - - 'Docker Compose' - - 'Kubernetes with Helm' + - "Docker Compose" + - "Kubernetes with Helm" - Kubernetes - Binary - Other id: deployment type: dropdown - attributes: - description: 'Add any other context about the problem here.' + description: "Add any other context about the problem here." label: Additional Context id: additional type: textarea diff --git a/.github/ISSUE_TEMPLATE/DESIGN-DOC.yml b/.github/ISSUE_TEMPLATE/DESIGN-DOC.yml index d26fb343332..69f87ac9346 100644 --- a/.github/ISSUE_TEMPLATE/DESIGN-DOC.yml +++ b/.github/ISSUE_TEMPLATE/DESIGN-DOC.yml @@ -1,8 +1,8 @@ description: - 'A design document is needed for non-trivial changes to the code base.' + "A design document is needed for non-trivial changes to the code base." labels: - rfc -name: 'Design Document' +name: "Design Document" body: - attributes: value: | @@ -18,11 +18,11 @@ body: after code reviews, and your pull requests will be merged faster. type: markdown - attributes: - label: 'Preflight checklist' + label: "Preflight checklist" options: - label: - 'I could not find a solution in the existing issues, docs, nor - discussions.' + "I could not find a solution in the existing issues, docs, nor + discussions." required: true - label: "I agree to follow this project's [Code of @@ -33,18 +33,18 @@ body: Guidelines](https://github.com/ory/hydra/blob/master/CONTRIBUTING.md)." required: true - label: - 'This issue affects my [Ory Cloud](https://www.ory.sh/) project.' + "This issue affects my [Ory Cloud](https://www.ory.sh/) project." - label: - 'I have joined the [Ory Community Slack](https://slack.ory.sh).' + "I have joined the [Ory Community Slack](https://slack.ory.sh)." - label: - 'I am signed up to the [Ory Security Patch - Newsletter](https://ory.us10.list-manage.com/subscribe?u=ffb1a878e4ec6c0ed312a3480&id=f605a41b53).' + "I am signed up to the [Ory Security Patch + Newsletter](https://ory.us10.list-manage.com/subscribe?u=ffb1a878e4ec6c0ed312a3480&id=f605a41b53)." id: checklist type: checkboxes - attributes: description: | This section gives the reader a very rough overview of the landscape in which the new system is being built and what is actually being built. This isn’t a requirements doc. Keep it succinct! The goal is that readers are brought up to speed but some previous knowledge can be assumed and detailed info can be linked to. This section should be entirely focused on objective background facts. - label: 'Context and scope' + label: "Context and scope" id: scope type: textarea validations: @@ -53,7 +53,7 @@ body: - attributes: description: | A short list of bullet points of what the goals of the system are, and, sometimes more importantly, what non-goals are. Note, that non-goals aren’t negated goals like “The system shouldn’t crash”, but rather things that could reasonably be goals, but are explicitly chosen not to be goals. A good example would be “ACID compliance”; when designing a database, you’d certainly want to know whether that is a goal or non-goal. And if it is a non-goal you might still select a solution that provides it, if it doesn’t introduce trade-offs that prevent achieving the goals. - label: 'Goals and non-goals' + label: "Goals and non-goals" id: goals type: textarea validations: @@ -65,7 +65,7 @@ body: The design doc is the place to write down the trade-offs you made in designing your software. Focus on those trade-offs to produce a useful document with long-term value. That is, given the context (facts), goals and non-goals (requirements), the design doc is the place to suggest solutions and show why a particular solution best satisfies those goals. The point of writing a document over a more formal medium is to provide the flexibility to express the problem set at hand in an appropriate manner. Because of this, there is no explicit guidance for how to actually describe the design. - label: 'The design' + label: "The design" id: design type: textarea validations: @@ -74,21 +74,21 @@ body: - attributes: description: | If the system under design exposes an API, then sketching out that API is usually a good idea. In most cases, however, one should withstand the temptation to copy-paste formal interface or data definitions into the doc as these are often verbose, contain unnecessary detail and quickly get out of date. Instead focus on the parts that are relevant to the design and its trade-offs. - label: 'APIs' + label: "APIs" id: apis type: textarea - attributes: description: | Systems that store data should likely discuss how and in what rough form this happens. Similar to the advice on APIs, and for the same reasons, copy-pasting complete schema definitions should be avoided. Instead focus on the parts that are relevant to the design and its trade-offs. - label: 'Data storage' + label: "Data storage" id: persistence type: textarea - attributes: description: | Design docs should rarely contain code, or pseudo-code except in situations where novel algorithms are described. As appropriate, link to prototypes that show the implementability of the design. - label: 'Code and pseudo-code' + label: "Code and pseudo-code" id: pseudocode type: textarea @@ -101,7 +101,7 @@ body: On the other end are systems where the possible solutions are very well defined, but it isn’t at all obvious how they could even be combined to achieve the goals. This may be a legacy system that is difficult to change and wasn’t designed to do what you want it to do or a library design that needs to operate within the constraints of the host programming language. In this situation you may be able to enumerate all the things you can do relatively easily, but you need to creatively put those things together to achieve the goals. There may be multiple solutions, and none of them are really great, and hence such a document should focus on selecting the best way given all identified trade-offs. - label: 'Degree of constraint' + label: "Degree of constraint" id: constrait type: textarea diff --git a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml index bcb03404bf3..6e7fb2b3319 100644 --- a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml +++ b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml @@ -1,8 +1,8 @@ description: - 'Suggest an idea for this project without a plan for implementation' + "Suggest an idea for this project without a plan for implementation" labels: - feat -name: 'Feature Request' +name: "Feature Request" body: - attributes: value: | @@ -11,11 +11,11 @@ body: If you already have a plan to implement a feature or a change, please create a [design document](https://github.com/aeneasr/gh-template-test/issues/new?assignees=&labels=rfc&template=DESIGN-DOC.yml) instead if the change is non-trivial! type: markdown - attributes: - label: 'Preflight checklist' + label: "Preflight checklist" options: - label: - 'I could not find a solution in the existing issues, docs, nor - discussions.' + "I could not find a solution in the existing issues, docs, nor + discussions." required: true - label: "I agree to follow this project's [Code of @@ -26,18 +26,18 @@ body: Guidelines](https://github.com/ory/hydra/blob/master/CONTRIBUTING.md)." required: true - label: - 'This issue affects my [Ory Cloud](https://www.ory.sh/) project.' + "This issue affects my [Ory Cloud](https://www.ory.sh/) project." - label: - 'I have joined the [Ory Community Slack](https://slack.ory.sh).' + "I have joined the [Ory Community Slack](https://slack.ory.sh)." - label: - 'I am signed up to the [Ory Security Patch - Newsletter](https://ory.us10.list-manage.com/subscribe?u=ffb1a878e4ec6c0ed312a3480&id=f605a41b53).' + "I am signed up to the [Ory Security Patch + Newsletter](https://ory.us10.list-manage.com/subscribe?u=ffb1a878e4ec6c0ed312a3480&id=f605a41b53)." id: checklist type: checkboxes - attributes: description: - 'Is your feature request related to a problem? Please describe.' - label: 'Describe your problem' + "Is your feature request related to a problem? Please describe." + label: "Describe your problem" placeholder: "A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]" @@ -50,20 +50,20 @@ body: Describe the solution you'd like placeholder: | A clear and concise description of what you want to happen. - label: 'Describe your ideal solution' + label: "Describe your ideal solution" id: solution type: textarea validations: required: true - attributes: description: "Describe alternatives you've considered" - label: 'Workarounds or alternatives' + label: "Workarounds or alternatives" id: alternatives type: textarea validations: required: true - attributes: - description: 'What version of our software are you running?' + description: "What version of our software are you running?" label: Version id: version type: input @@ -71,7 +71,7 @@ body: required: true - attributes: description: - 'Add any other context or screenshots about the feature request here.' + "Add any other context or screenshots about the feature request here." label: Additional Context id: additional type: textarea diff --git a/.github/config.yml b/.github/config.yml index 0d121fe184f..ea335697979 100644 --- a/.github/config.yml +++ b/.github/config.yml @@ -1,3 +1,3 @@ todo: - keyword: '@todo' + keyword: "@todo" label: todo diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d8bcb167f09..d22b92a3142 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -41,9 +41,9 @@ If you're unsure about any of them, don't hesitate to ask. We're here to help! - [ ] I have referenced an issue containing the design document if my change introduces a new feature. - [ ] I am following the [contributing code guidelines](../blob/master/CONTRIBUTING.md#contributing-code). - [ ] I have read the [security policy](../security/policy). -- [ ] I confirm that this pull request does not address a security vulnerability. If this pull request addresses a security. - vulnerability, I confirm that I got green light (please contact [security@ory.sh](mailto:security@ory.sh)) from the - maintainers to push the changes. +- [ ] I confirm that this pull request does not address a security vulnerability. + If this pull request addresses a security. vulnerability, + I confirm that I got green light (please contact [security@ory.sh](mailto:security@ory.sh)) from the maintainers to push the changes. - [ ] I have added tests that prove my fix is effective or that my feature works. - [ ] I have added or changed [the documentation](https://github.com/ory/docs). diff --git a/.github/semantic.yml b/.github/semantic.yml deleted file mode 100644 index 4f8499c9f1d..00000000000 --- a/.github/semantic.yml +++ /dev/null @@ -1,17 +0,0 @@ -titleOnly: true -commitsOnly: false -titleAndCommits: false - -types: - - feat - - fix - - revert - - docs - - style - - refactor - - test - - build - - autogen - - security - - ci - - chore diff --git a/.github/workflows/closed_references.yml b/.github/workflows/closed_references.yml index ebafc8a71af..2789ac42c2c 100644 --- a/.github/workflows/closed_references.yml +++ b/.github/workflows/closed_references.yml @@ -2,13 +2,13 @@ name: Closed Reference Notifier on: schedule: - - cron: '0 0 * * *' + - cron: "0 0 * * *" workflow_dispatch: inputs: issueLimit: description: Max. number of issues to create required: true - default: '5' + default: "5" jobs: find_closed_references: @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v2-beta with: - node-version: '14' + node-version: "14" - uses: ory/closed-reference-notifier@v1 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/conventional_commits.yml b/.github/workflows/conventional_commits.yml new file mode 100644 index 00000000000..9d92dcb8884 --- /dev/null +++ b/.github/workflows/conventional_commits.yml @@ -0,0 +1,75 @@ +name: Conventional commits + +on: + pull_request_target: # enable Pull Requests from forks, uses config from master branch + types: [opened, edited, reopened, ready_for_review] + # pull_request: # for debugging, uses config in local branch but supports only Pull Requests from this repo + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v4 + id: check-title + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + revert + docs + style + refactor + test + build + autogen + security + ci + chore + + scopes: | + blog + cms + deps + docs + home + hydra + keto + kratos + stats + + requireScope: false + + # Configure which scopes are disallowed in PR titles. For instance by setting + # the value below, `chore(release): ...` and `ci(e2e,release): ...` will be rejected. + # disallowScopes: | + # release + + # Configure additional validation for the subject based on a regex. + # This example ensures the subject doesn't start with an uppercase character. + subjectPattern: ^(?![A-Z]).+$ + + # If `subjectPattern` is configured, you can use this property to override + # the default error message that is shown when the pattern doesn't match. + # The variables `subject` and `title` can be used within the message. + subjectPatternError: | + The subject should start with a lowercase letter, yours is uppercase: + "{subject}" + + # If the PR contains one of these labels, the validation is skipped. + # Multiple labels can be separated by newlines. + # If you want to rerun the validation when labels change, you might want + # to use the `labeled` and `unlabeled` event triggers in your workflow. + # ignoreLabels: | + # bot + # ignore-semantic-pull-request + + # For work-in-progress PRs you can typically use draft pull requests + # from GitHub. However, private repositories on the free plan don't have + # this option and therefore this action allows you to opt-in to using the + # special "[WIP]" prefix to indicate this state. This will avoid the + # validation of the PR title and the pull request checks remain pending. + # Note that a second check will be reported if this is enabled. + # wip: true diff --git a/.github/workflows/milestone.yml b/.github/workflows/milestone.yml index b4a30699f01..fb47e4a78f0 100644 --- a/.github/workflows/milestone.yml +++ b/.github/workflows/milestone.yml @@ -3,7 +3,7 @@ name: Generate and Publish Milestone Document on: workflow_dispatch: schedule: - - cron: '0 0 * * *' + - cron: "0 0 * * *" jobs: milestone: @@ -23,8 +23,8 @@ jobs: - name: Commit Milestone Documentation uses: EndBug/add-and-commit@v4.4.0 with: - message: 'autogen(docs): update milestone document' + message: "autogen(docs): update milestone document" author_name: aeneasr - author_email: '3372410+aeneasr@users.noreply.github.com' + author_email: "3372410+aeneasr@users.noreply.github.com" env: GITHUB_TOKEN: ${{ secrets.TOKEN_PRIVILEGED }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 395cb69206d..eb36db174bb 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,8 +1,8 @@ -name: 'Close Stale Issues' +name: "Close Stale Issues" on: workflow_dispatch: schedule: - - cron: '0 0 * * *' + - cron: "0 0 * * *" jobs: stale: @@ -35,10 +35,10 @@ jobs: Thank you for your understanding and to anyone who participated in the conversation! And as written above, please do participate in the conversation if this topic is important to you! Thank you 🙏✌️ - stale-issue-label: 'stale' - exempt-issue-labels: 'bug,blocking,docs,backlog' + stale-issue-label: "stale" + exempt-issue-labels: "bug,blocking,docs,backlog" days-before-stale: 365 days-before-close: 30 exempt-milestones: true exempt-assignees: true - only-pr-labels: 'stale' + only-pr-labels: "stale" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index f9ab1ecc4db..da4b27661c7 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,60 +2,131 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation -in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, -sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment include: +Examples of behavior that contributes to a positive environment for our +community include: -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -- The use of sexualized language or imagery and unwelcome sexual attention or advances -- Trolling, insulting/derogatory comments, and personal or political attacks +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment -- Publishing others' private information, such as a physical or electronic address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a professional setting +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and -fair corrective action in response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and -other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other -behaviors that they deem inappropriate, threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its -community. Examples of representing a project or community include using an official project e-mail address, posting via an -official social media account, or acting as an appointed representative at an online or offline event. Representation of a project -may be further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at -office@ory.sh. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and -appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an -incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[office@ory.sh](mailto:office@ory.sh). All complaints will be reviewed and +investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions -as determined by other members of the project's leadership. +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at -https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. -[homepage]: https://www.contributor-covenant.org +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder][mozilla coc]. -For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][faq]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[mozilla coc]: https://github.com/mozilla/diversity +[faq]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 08b796a088b..94d332597fd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ https://github.com/ory/meta/blob/master/templates/repository/common/CONTRIBUTING --> -# Contributing to Ory Hydra +# Contribute to Ory Hydra @@ -17,48 +17,56 @@ https://github.com/ory/meta/blob/master/templates/repository/common/CONTRIBUTING - [FAQ](#faq) - [How can I contribute?](#how-can-i-contribute) - [Communication](#communication) -- [Contributing Code](#contributing-code) -- [Documentation](#documentation) +- [Contribute examples](#contribute-examples) +- [Contribute code](#contribute-code) +- [Contribute documentation](#contribute-documentation) - [Disclosing vulnerabilities](#disclosing-vulnerabilities) -- [Code Style](#code-style) - - [Working with Forks](#working-with-forks) +- [Code style](#code-style) + - [Working with forks](#working-with-forks) - [Conduct](#conduct) ## Introduction -There are many ways in which you can contribute, beyond writing code. The goal of this document is to provide a high-level -overview of how you can get involved. +_Please note_: We take Ory Hydra's security and our users' trust very +seriously. If you believe you have found a security issue in Ory Hydra, +please disclose by contacting us at security@ory.sh. -_Please note_: We take Ory Hydra's security and our users' trust very seriously. If you believe you have found a security issue -in Ory Hydra, please responsibly disclose by contacting us at security@ory.sh. +There are many ways in which you can contribute. The goal of this document is to +provide a high-level overview of how you can get involved in Ory. -First: As a potential contributor, your changes and ideas are welcome at any hour of the day or night, weekdays, weekends, and -holidays. Please do not ever hesitate to ask a question or send a pull request. +As a potential contributor, your changes and ideas are welcome at any hour of +the day or night, weekdays, weekends, and holidays. Please do not ever hesitate +to ask a question or send a pull request. -If you are unsure, just ask or submit the issue or pull request anyways. You won't be yelled at for giving it your best effort. -The worst that can happen is that you'll be politely asked to change something. We appreciate any sort of contributions, and don't -want a wall of rules to get in the way of that. +If you are unsure, just ask or submit the issue or pull request anyways. You +won't be yelled at for giving it your best effort. The worst that can happen is +that you'll be politely asked to change something. We appreciate any sort of +contributions, and don't want a wall of rules to get in the way of that. -That said, if you want to ensure that a pull request is likely to be merged, talk to us! You can find out our thoughts and ensure -that your contribution won't clash or be obviated by Ory -Hydra's normal direction. A great way to +That said, if you want to ensure that a pull request is likely to be merged, +talk to us! You can find out our thoughts and ensure that your contribution +won't clash with Ory +Hydra's direction. A great way to do this is via -[Ory Hydra Discussions](https://github.com/ory/hydra/discussions) or the -[Ory Chat](https://www.ory.sh/chat). +[Ory Hydra Discussions](https://github.com/ory/hydra/discussions) +or the [Ory Chat](https://www.ory.sh/chat). ## FAQ - I am new to the community. Where can I find the [Ory Community Code of Conduct?](https://github.com/ory/hydra/blob/master/CODE_OF_CONDUCT.md) -- I have a question. Where can I get [answers to questions regarding Ory Hydra?](#communication) +- I have a question. Where can I get + [answers to questions regarding Ory Hydra?](#communication) -- I would like to contribute but I am not sure how. Are there [easy ways to contribute?](#how-can-i-contribute) +- I would like to contribute but I am not sure how. Are there + [easy ways to contribute?](#how-can-i-contribute) [Or good first issues?](https://github.com/search?l=&o=desc&q=label%3A%22help+wanted%22+label%3A%22good+first+issue%22+is%3Aopen+user%3Aory+user%3Aory-corp&s=updated&type=Issues) -- I want to talk to other Ory Hydra users. [How can I become a part of the community?](#communication) +- I want to talk to other Ory Hydra users. + [How can I become a part of the community?](#communication) - I would like to know what I am agreeing to when I contribute to Ory Hydra. @@ -70,66 +78,117 @@ do this is via ## How can I contribute? -If you want to start contributing code right away, we have a +If you want to start to contribute code right away, take a look at the [list of good first issues](https://github.com/ory/hydra/labels/good%20first%20issue). -There are many other ways you can contribute without writing any code. Here are a few things you can do to help out: +There are many other ways you can contribute. Here are a few things you can do +to help out: -- **Give us a star.** It may not seem like much, but it really makes a difference. This is something that everyone can do to help - out Ory Hydra. Github stars help the project gain visibility and stand out. +- **Give us a star.** It may not seem like much, but it really makes a + difference. This is something that everyone can do to help out Ory Hydra. + Github stars help the project gain visibility and stand out. -- **Join the community.** Sometimes helping people can be as easy as listening to their problems and offering a different - perspective. Join our Slack, have a look at discussions in the forum and take part in our weekly hangout. More info on this in - [Communication](#communication). +- **Join the community.** Sometimes helping people can be as easy as listening + to their problems and offering a different perspective. Join our Slack, have a + look at discussions in the forum and take part in community events. More info + on this in [Communication](#communication). -- **Helping with open issues.** We have a lot of open issues for Ory Hydra and some of them may lack necessary information, - some are duplicates of older issues. You can help out by guiding people through the process of filling out the issue template, - asking for clarifying information, or pointing them to existing issues that match their description of the problem. +- **Answer discussions.** There are at all times a number of unanswered + discussions on GitHub, you can see an + [overview here](https://github.com/discussions?discussions_q=is%3Aunanswered+org%3Aory+sort%3Aupdated-desc). + If you think you know an answer or can provide some information that might + help, please share it! Bonus: You get GitHub achievements for answered + discussions. -- **Reviewing documentation changes.** Most documentation just needs a review for proper spelling and grammar. If you think a - document can be improved in any way, feel free to hit the `edit` button at the top of the page. More info on contributing to - documentation [here](#documentation). +- **Help with open issues.** We have a lot of open issues for Ory Hydra and + some of them may lack necessary information, some are duplicates of older + issues. You can help out by guiding people through the process of filling out + the issue template, asking for clarifying information, or pointing them to + existing issues that match their description of the problem. -- **Help with tests.** Some pull requests may lack proper tests or test plans. These are needed for the change to be implemented - safely. +- **Review documentation changes.** Most documentation just needs a review for + proper spelling and grammar. If you think a document can be improved in any + way, feel free to hit the `edit` button at the top of the page. More info on + contributing to documentation [here](#contribute-documentation). + +- **Help with tests.** Pull requests may lack proper tests or test plans. These + are needed for the change to be implemented safely. ## Communication -We use [Slack](https://www.ory.sh/chat). You are welcome to drop in and ask questions, discuss bugs and feature requests, talk to -other users of Ory, etc. +We use [Slack](https://www.ory.sh/chat). You are welcome to drop in and ask +questions, discuss bugs and feature requests, talk to other users of Ory, etc. -Check out [Ory Hydra Discussions](https://github.com/ory/hydra/discussions). This is a great place for in-depth discussions and lots of code examples, logs -and similar data. +Check out [Ory Hydra Discussions](https://github.com/ory/hydra/discussions). This is a great place for +in-depth discussions and lots of code examples, logs and similar data. -You can also join our community hangout, if you want to speak to the Ory team directly or ask some questions. You can find more -info on the hangouts in [Slack](https://www.ory.sh/chat). +You can also join our community calls, if you want to speak to the Ory team +directly or ask some questions. You can find more info and participate in +[Slack](https://www.ory.sh/chat) in the #community-call channel. -If you want to receive regular notifications about updates to Ory Hydra, consider joining the mailing list. We will _only_ send -you vital information on the projects that you are interested in. +If you want to receive regular notifications about updates to Ory Hydra, +consider joining the mailing list. We will _only_ send you vital information on +the projects that you are interested in. Also [follow us on twitter](https://twitter.com/orycorp). -## Contributing Code - -Unless you are fixing a known bug, we **strongly** recommend discussing it with the core team via a GitHub issue or -[in our chat](https://www.ory.sh/chat) before getting started to ensure your work is consistent with Ory Hydra's roadmap and -architecture. - -All contributions are made via pull requests. To make a pull request, you will need a GitHub account; if you are unclear on this -process, see GitHub's documentation on [forking](https://help.github.com/articles/fork-a-repo) and -[pull requests](https://help.github.com/articles/using-pull-requests). Pull requests should be targeted at the `master` branch. -Before creating a pull request, go through this checklist: +## Contribute examples + +One of the most impactful ways to make a contribution is adding examples. You +can find an overview of examples using Ory services in the +[documentation examples page](https://www.ory.sh/docs/examples). Source code for +examples can be found in most cases in the +[ory/examples](https://github.com/ory/examples) repository. + +_If you would like to contribute a new example, we would love to hear from you!_ + +Please [open an issue](https://github.com/ory/examples/issues/new/choose) to +describe your example before you start working on it. We would love to provide +guidance to make for a pleasant contribution experience. Go through this +checklist to contribute an example: + +1. Create a github issue proposing a new example and make sure it's different + from an existing one. +1. Fork the repo and create a feature branch off of `master` so that changes do + not get mixed up. +1. Add a descriptive prefix to commits. This ensures a uniform commit history + and helps structure the changelog. Please refer to this + [list of prefixes for Hydra](https://github.com/ory/hydra/blob/master/.github/semantic.yml) + for an overview. +1. Create a `README.md` that explains how to use the example. (Use + [the README template](https://github.com/ory/examples/blob/master/_common/README)). +1. Open a pull request and maintainers will review and merge your example. + +## Contribute code + +Unless you are fixing a known bug, we **strongly** recommend discussing it with +the core team via a GitHub issue or [in our chat](https://www.ory.sh/chat) +before getting started to ensure your work is consistent with Ory Hydra's +roadmap and architecture. + +All contributions are made via pull requests. To make a pull request, you will +need a GitHub account; if you are unclear on this process, see GitHub's +documentation on [forking](https://help.github.com/articles/fork-a-repo) and +[pull requests](https://help.github.com/articles/using-pull-requests). Pull +requests should be targeted at the `master` branch. Before creating a pull +request, go through this checklist: 1. Create a feature branch off of `master` so that changes do not get mixed up. -1. [Rebase](http://git-scm.com/book/en/Git-Branching-Rebasing) your local changes against the `master` branch. -1. Run the full project test suite with the `go test -tags sqlite ./...` (or equivalent) command and confirm that it passes. -1. Run `make format` if a `Makefile` is available, `gofmt -s` if the project is written in Go, `npm run format` if the project is - written for NodeJS. -1. Ensure that each commit has a descriptive prefix. This ensures a uniform commit history and helps structure the changelog. - Please refer to this [list of prefixes for Hydra](https://github.com/ory/hydra/blob/master/.github/semantic.yml) for an - overview. -1. Sign-up with CircleCI so that it has access to your repository with the branch containing your PR. Simply creating a CircleCI - account is sufficient for the CI jobs to run, you do not need to setup a CircleCI project for the branch. +1. [Rebase](http://git-scm.com/book/en/Git-Branching-Rebasing) your local + changes against the `master` branch. +1. Run the full project test suite with the `go test -tags sqlite ./...` (or + equivalent) command and confirm that it passes. +1. Run `make format` if a `Makefile` is available, `gofmt -s` if the project is + written in Go, `npm run format` if the project is written for NodeJS. +1. Add a descriptive prefix to commits. This ensures a uniform commit history + and helps structure the changelog. + Please refer to this + [list of prefixes for Hydra](https://github.com/ory/hydra/blob/master/.github/semantic.yml) + for an overview. +1. Sign-up with CircleCI so that it has access to your repository with the + branch containing your PR. Simply creating a CircleCI account is sufficient + for the CI jobs to run, you do not need to setup a CircleCI project for the + branch. If a pull request is not ready to be reviewed yet [it should be marked as a "Draft"](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request). @@ -137,50 +196,57 @@ If a pull request is not ready to be reviewed yet Before your contributions can be reviewed you need to sign our [Contributor License Agreement](https://cla-assistant.io/ory/hydra). -This agreement defines the terms under which your code is contributed to Ory. More specifically it declares that you have the -right to, and actually do, grant us the rights to use your contribution. You can see the Apache 2.0 license under which our -projects are published [here](https://github.com/ory/meta/blob/master/LICENSE). +This agreement defines the terms under which your code is contributed to Ory. +More specifically it declares that you have the right to, and actually do, grant +us the rights to use your contribution. You can see the Apache 2.0 license under +which our projects are published +[here](https://github.com/ory/meta/blob/master/LICENSE). -When pull requests fail testing, authors are expected to update their pull requests to address the failures until the tests pass. +When pull requests fail testing, authors are expected to update their pull +requests to address the failures until the tests pass. Pull requests eligible for review 1. follow the repository's code formatting conventions; -2. include tests which prove that the change works as intended and does not add regressions; +2. include tests which prove that the change works as intended and does not add + regressions; 3. document the changes in the code and/or the project's documentation; 4. pass the CI pipeline; -5. have signed our [Contributor License Agreement](https://cla-assistant.io/ory/hydra); +5. have signed our + [Contributor License Agreement](https://cla-assistant.io/ory/hydra); 6. include a proper git commit message following the [Conventional Commit Specification](https://www.conventionalcommits.org/en/v1.0.0/). -If all of these items are checked, the pull request is ready to be reviewed and you should change the status to "Ready for review" -and +If all of these items are checked, the pull request is ready to be reviewed and +you should change the status to "Ready for review" and [request review from a maintainer](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review). Reviewers will approve the pull request once they are satisfied with the patch. -## Documentation - -Please provide documentation when changing, removing, or adding features. Documentation resides in the project's -[docs](https://github.com/ory/hydra/tree/master/docs) folder. Generate API and configuration reference documentation using -`cd docs; npm run gen`. +## Contribute documentation -For further instructions please head over to [docs/README.md](https://github.com/ory/hydra/blob/master/README.md). +Please provide documentation when changing, removing, or adding features. All +Ory Documentation resides in the +[Ory documentation repository](https://github.com/ory/docs/). For further +instructions please head over to the Ory Documentation +[README.md](https://github.com/ory/docs/blob/master/README.md). ## Disclosing vulnerabilities -Please disclose vulnerabilities exclusively to [security@ory.sh](mailto:security@ory.sh). Do not use GitHub issues. +Please disclose vulnerabilities exclusively to +[security@ory.sh](mailto:security@ory.sh). Do not use GitHub issues. -## Code Style +## Code style Please follow these guidelines when formatting source code: - Go code should match the output of `gofmt -s` and pass `golangci-lint run`. -- NodeJS and JavaScript code should be prettified using `npm run format` where appropriate. +- NodeJS and JavaScript code should be prettified using `npm run format` where + appropriate. -### Working with Forks +### Working with forks -``` +```bash # First you clone the original repository git clone git@github.com:ory/ory/hydra.git @@ -207,19 +273,11 @@ Now go to the project's GitHub Pull Request page and click "New pull request" ## Conduct -Whether you are a regular contributor or a newcomer, we care about making this community a safe place for you and we've got your -back. - -- We are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, - disability, ethnicity, religion, or similar personal characteristic. -- Please avoid using nicknames that might detract from a friendly, safe and welcoming environment for all. -- Be kind and courteous. There is no need to be mean or rude. -- We will exclude you from interaction if you insult, demean or harass anyone. In particular, we do not tolerate behavior that - excludes people in socially marginalized groups. -- Private harassment is also unacceptable. No matter who you are, if you feel you have been or are being harassed or made - uncomfortable by a community member, please contact one of the channel ops or a member of the Ory Hydra core team - immediately. -- Likewise any spamming, trolling, flaming, baiting or other attention-stealing behaviour is not welcome. - -We welcome discussion about creating a welcoming, safe, and productive environment for the community. If you have any questions, -feedback, or concerns [please let us know](https://www.ory.sh/chat). +Whether you are a regular contributor or a newcomer, we care about making this +community a safe place for you and we've got your back. + +[Ory Community Code of Conduct](https://github.com/ory/hydra/blob/master/CODE_OF_CONDUCT.md) + +We welcome discussion about creating a welcoming, safe, and productive +environment for the community. If you have any questions, feedback, or concerns +[please let us know](https://www.ory.sh/chat). diff --git a/README.md b/README.md index 966277254a3..cebf6df0b15 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ ---

- Build Status + CI Tasks for Ory Hydra Go Report Card PkgGoDev @@ -111,17 +111,21 @@ to verify user consent allowing you to use Ory Hydra with any authentication end -The Ory community stands on the shoulders of individuals, companies, and maintainers. We thank everyone involved - from submitting -bug reports and feature requests, to contributing patches, to sponsoring our work. Our community is 1000+ strong and growing -rapidly. The Ory stack protects 16.000.000.000+ API requests every month with over 250.000+ active service nodes. We would have +The Ory community stands on the shoulders of individuals, companies, and +maintainers. We thank everyone involved - from submitting bug reports and +feature requests, to contributing patches, to sponsoring our work. Our community +is 1000+ strong and growing rapidly. The Ory stack protects 16.000.000.000+ API +requests every month with over 250.000+ active service nodes. We would have never been able to achieve this without each and everyone of you! -The following list represents companies that have accompanied us along the way and that have made outstanding contributions to our -ecosystem. _If you think that your company deserves a spot here, reach out to +The following list represents companies that have accompanied us along the way +and that have made outstanding contributions to our ecosystem. _If you think +that your company deserves a spot here, reach out to office-muc@ory.sh now_! -**Please consider giving back by becoming a sponsor of our open source work on Patreon -or Open Collective.** +**Please consider giving back by becoming a sponsor of our open source work on +Patreon or +Open Collective.** @@ -277,6 +281,12 @@ or Open Collective.** + + + + + +
Buhta buhta.com
Adopter *ConnctdConnctdconnctd.com
@@ -288,8 +298,10 @@ as well as all of our backers -and past & current supporters (in alphabetical order) on [Patreon](https://www.patreon.com/_ory): Alexander Alimovs, Billy, Chancy -Kennedy, Drozzy, Edwin Trejos, Howard Edidin, Ken Adler Oz Haven, Stefan Hans, TheCrealm. +and past & current supporters (in alphabetical order) on +[Patreon](https://www.patreon.com/_ory): Alexander Alimovs, Billy, Chancy +Kennedy, Drozzy, Edwin Trejos, Howard Edidin, Ken Adler Oz Haven, Stefan Hans, +TheCrealm. \* Uses one of Ory's major projects in production. @@ -361,42 +373,51 @@ Head over to the [Ory Developer Documentation](https://www.ory.sh/docs/hydra/ins -We build Ory on several guiding principles when it comes to our architecture design: +We build Ory on several guiding principles when it comes to our architecture +design: - Minimal dependencies - Runs everywhere - Scales without effort - Minimize room for human and network errors -Ory's architecture is designed to run best on a Container Orchestration system such as Kubernetes, CloudFoundry, OpenShift, and -similar projects. Binaries are small (5-15MB) and available for all popular processor types (ARM, AMD64, i386) and operating -systems (FreeBSD, Linux, macOS, Windows) without system dependencies (Java, Node, Ruby, libxml, ...). +Ory's architecture is designed to run best on a Container Orchestration system +such as Kubernetes, CloudFoundry, OpenShift, and similar projects. Binaries are +small (5-15MB) and available for all popular processor types (ARM, AMD64, i386) +and operating systems (FreeBSD, Linux, macOS, Windows) without system +dependencies (Java, Node, Ruby, libxml, ...). ### Ory Kratos: Identity and User Infrastructure and Management -[Ory Kratos](https://github.com/ory/kratos) is an API-first Identity and User Management system that is built according to -[cloud architecture best practices](https://www.ory.sh/docs/next/ecosystem/software-architecture-philosophy). It implements core -use cases that almost every software application needs to deal with: Self-service Login and Registration, Multi-Factor -Authentication (MFA/2FA), Account Recovery and Verification, Profile, and Account Management. +[Ory Kratos](https://github.com/ory/kratos) is an API-first Identity and User +Management system that is built according to +[cloud architecture best practices](https://www.ory.sh/docs/next/ecosystem/software-architecture-philosophy). +It implements core use cases that almost every software application needs to +deal with: Self-service Login and Registration, Multi-Factor Authentication +(MFA/2FA), Account Recovery and Verification, Profile, and Account Management. ### Ory Hydra: OAuth2 & OpenID Connect Server -[Ory Hydra](https://github.com/ory/hydra) is an OpenID Certified™ OAuth2 and OpenID Connect Provider which easily connects to any -existing identity system by writing a tiny "bridge" application. Gives absolute control over user interface and user experience -flows. +[Ory Hydra](https://github.com/ory/hydra) is an OpenID Certified™ OAuth2 and +OpenID Connect Provider which easily connects to any existing identity system by +writing a tiny "bridge" application. Gives absolute control over user interface +and user experience flows. ### Ory Oathkeeper: Identity & Access Proxy -[Ory Oathkeeper](https://github.com/ory/oathkeeper) is a BeyondCorp/Zero Trust Identity & Access Proxy (IAP) with configurable -authentication, authorization, and request mutation rules for your web services: Authenticate JWT, Access Tokens, API Keys, mTLS; -Check if the contained subject is allowed to perform the request; Encode resulting content into custom headers (`X-User-ID`), JSON -Web Tokens and more! +[Ory Oathkeeper](https://github.com/ory/oathkeeper) is a BeyondCorp/Zero Trust +Identity & Access Proxy (IAP) with configurable authentication, authorization, +and request mutation rules for your web services: Authenticate JWT, Access +Tokens, API Keys, mTLS; Check if the contained subject is allowed to perform the +request; Encode resulting content into custom headers (`X-User-ID`), JSON Web +Tokens and more! ### Ory Keto: Access Control Policies as a Server -[Ory Keto](https://github.com/ory/keto) is a policy decision point. It uses a set of access control policies, similar to AWS IAM -Policies, in order to determine whether a subject (user, application, service, car, ...) is authorized to perform a certain action -on a resource. +[Ory Keto](https://github.com/ory/keto) is a policy decision point. It uses a +set of access control policies, similar to AWS IAM Policies, in order to +determine whether a subject (user, application, service, car, ...) is authorized +to perform a certain action on a resource. diff --git a/SECURITY.md b/SECURITY.md index 8152c97a563..70f1ef4ddb7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -21,8 +21,8 @@ https://github.com/ory/meta/blob/master/templates/repository/SECURITY.md ## Supported Versions -We release patches for security vulnerabilities. Which versions are eligible receiving such patches depend on the CVSS v3.0 -Rating: +We release patches for security vulnerabilities. Which versions are eligible +receiving such patches depend on the CVSS v3.0 Rating: | CVSS v3.0 | Supported Versions | | --------- | ----------------------------------------- | @@ -31,6 +31,7 @@ Rating: ## Reporting a Vulnerability -Please report (suspected) security vulnerabilities to **[security@ory.sh](mailto:security@ory.sh)**. You will receive a response -from us within 48 hours. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but -historically within a few days. +Please report (suspected) security vulnerabilities to +**[security@ory.sh](mailto:security@ory.sh)**. You will receive a response from +us within 48 hours. If the issue is confirmed, we will release a patch as soon +as possible depending on complexity but historically within a few days. diff --git a/client/handler.go b/client/handler.go index 7ede94a686a..c0e9eff4429 100644 --- a/client/handler.go +++ b/client/handler.go @@ -30,6 +30,7 @@ import ( "github.com/pborman/uuid" + "github.com/ory/x/jsonx" "github.com/ory/x/urlx" "github.com/ory/fosite" @@ -356,7 +357,7 @@ func (h *Handler) Patch(w http.ResponseWriter, r *http.Request, ps httprouter.Pa oldSecret := c.Secret - if err := x.ApplyJSONPatch(patchJSON, c, "/id"); err != nil { + if err := jsonx.ApplyJSONPatch(patchJSON, c, "/id"); err != nil { h.r.Writer().WriteError(w, r, err) return } diff --git a/cmd/token_user.go b/cmd/token_user.go index 8179fe4b96c..571c98edc4c 100644 --- a/cmd/token_user.go +++ b/cmd/token_user.go @@ -174,7 +174,7 @@ and success, unless if the --no-shutdown flag is provided.`, fmt.Println("Setting up home route on " + serverLocation) fmt.Println("Setting up callback listener on " + serverLocation + "callback") - fmt.Println("Press ctrl + c on Linux / Windows or cmd + c on OSX to end the process.") + fmt.Println("Press ctrl + c to end the process.") fmt.Printf("If your browser does not open automatically, navigate to:\n\n\t%s\n\n", serverLocation) r := httprouter.New() diff --git a/quickstart-tracing.yml b/quickstart-tracing.yml index b1dd048bbbf..c4e327228e9 100644 --- a/quickstart-tracing.yml +++ b/quickstart-tracing.yml @@ -20,6 +20,7 @@ services: # - zipkin # - datadog environment: + # - TRACING_SERVICE_NAME="Ory Hydra" - TRACING_PROVIDER=jaeger # - TRACING_PROVIDER=zipkin # - TRACING_PROVIDER=datadog @@ -42,7 +43,6 @@ services: # - ELASTIC_APM_ENVIRONMENT="devel" ### Opentelemetry ### ### See env vars here: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md ### - # - OTEL_SERVICE_NAME="Ory Hydra" jaeger: image: jaegertracing/all-in-one:1.19.2 ports: diff --git a/x/json.go b/x/json.go deleted file mode 100644 index dfe770632da..00000000000 --- a/x/json.go +++ /dev/null @@ -1,45 +0,0 @@ -package x - -import ( - "encoding/json" - "fmt" - - jsonpatch "github.com/evanphx/json-patch" -) - -func ApplyJSONPatch(p json.RawMessage, object interface{}, denyPaths ...string) error { - patch, err := jsonpatch.DecodePatch(p) - if err != nil { - return err - } - - denySet := make(map[string]struct{}) - for _, path := range denyPaths { - denySet[path] = struct{}{} - } - - for _, op := range patch { - path, err := op.Path() - if err != nil { - return fmt.Errorf("error parsing patch operations: %v", err) - } - if _, ok := denySet[path]; ok { - return fmt.Errorf("patch includes denied path: %s", path) - } - } - - original, err := json.Marshal(object) - if err != nil { - return err - } - - modified, err := patch.Apply(original) - if err != nil { - return err - } - - if err := json.Unmarshal(modified, object); err != nil { - return err - } - return nil -} diff --git a/x/json_test.go b/x/json_test.go deleted file mode 100644 index 61d28614c8f..00000000000 --- a/x/json_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package x - -import ( - "testing" - - "github.com/mohae/deepcopy" - "github.com/stretchr/testify/require" -) - -type TestType struct { - Field1 string - Field2 []string - Field3 struct { - Field1 bool - Field2 []int - } -} - -func TestApplyJSONPatch(t *testing.T) { - object := TestType{ - Field1: "foo", - Field2: []string{ - "foo", - "bar", - "baz", - "kaz", - }, - Field3: struct { - Field1 bool - Field2 []int - }{ - Field1: true, - Field2: []int{ - 1, - 2, - 3, - }, - }, - } - t.Run("case=empty patch", func(t *testing.T) { - rawPatch := []byte(`[]`) - obj := deepcopy.Copy(object).(TestType) - require.NoError(t, ApplyJSONPatch(rawPatch, &obj)) - require.Equal(t, object, obj) - }) - t.Run("case=field replace", func(t *testing.T) { - rawPatch := []byte(`[{"op": "replace", "path": "/Field1", "value": "boo"}]`) - expected := deepcopy.Copy(object).(TestType) - expected.Field1 = "boo" - obj := deepcopy.Copy(object).(TestType) - require.NoError(t, ApplyJSONPatch(rawPatch, &obj)) - require.Equal(t, expected, obj) - }) - t.Run("case=array replace", func(t *testing.T) { - rawPatch := []byte(`[{"op": "replace", "path": "/Field2/0", "value": "boo"}]`) - expected := deepcopy.Copy(object).(TestType) - expected.Field2[0] = "boo" - obj := deepcopy.Copy(object).(TestType) - require.NoError(t, ApplyJSONPatch(rawPatch, &obj)) - require.Equal(t, expected, obj) - }) - t.Run("case=array append", func(t *testing.T) { - rawPatch := []byte(`[{"op": "add", "path": "/Field2/-", "value": "boo"}]`) - expected := deepcopy.Copy(object).(TestType) - expected.Field2 = append(expected.Field2, "boo") - obj := deepcopy.Copy(object).(TestType) - require.NoError(t, ApplyJSONPatch(rawPatch, &obj)) - require.Equal(t, expected, obj) - }) - t.Run("case=array remove", func(t *testing.T) { - rawPatch := []byte(`[{"op": "remove", "path": "/Field2/0"}]`) - expected := deepcopy.Copy(object).(TestType) - expected.Field2 = expected.Field2[1:] - obj := deepcopy.Copy(object).(TestType) - require.NoError(t, ApplyJSONPatch(rawPatch, &obj)) - require.Equal(t, expected, obj) - }) - t.Run("case=nested field replace", func(t *testing.T) { - rawPatch := []byte(`[{"op": "replace", "path": "/Field3/Field1", "value": false}]`) - expected := deepcopy.Copy(object).(TestType) - expected.Field3.Field1 = false - obj := deepcopy.Copy(object).(TestType) - require.NoError(t, ApplyJSONPatch(rawPatch, &obj)) - require.Equal(t, expected, obj) - }) - t.Run("case=nested array append", func(t *testing.T) { - rawPatch := []byte(`[{"op": "add", "path": "/Field3/Field2/-", "value": 4}]`) - expected := deepcopy.Copy(object).(TestType) - expected.Field3.Field2 = append(expected.Field3.Field2, 4) - obj := deepcopy.Copy(object).(TestType) - require.NoError(t, ApplyJSONPatch(rawPatch, &obj)) - require.Equal(t, expected, obj) - }) - t.Run("case=nested array remove", func(t *testing.T) { - rawPatch := []byte(`[{"op": "remove", "path": "/Field3/Field2/2"}]`) - expected := deepcopy.Copy(object).(TestType) - expected.Field3.Field2 = expected.Field3.Field2[:2] - obj := deepcopy.Copy(object).(TestType) - require.NoError(t, ApplyJSONPatch(rawPatch, &obj)) - require.Equal(t, expected, obj) - }) - t.Run("case=patch denied path", func(t *testing.T) { - rawPatch := []byte(`[{"op": "replace", "path": "/Field1", "value": "bar"}]`) - obj := deepcopy.Copy(object).(TestType) - require.Error(t, ApplyJSONPatch(rawPatch, &obj, "/Field1")) - require.Equal(t, object, obj) - }) - t.Run("case=patch allowed path", func(t *testing.T) { - rawPatch := []byte(`[{"op": "add", "path": "/Field2/-", "value": "bar"}]`) - expected := deepcopy.Copy(object).(TestType) - expected.Field2 = append(expected.Field2, "bar") - obj := deepcopy.Copy(object).(TestType) - require.NoError(t, ApplyJSONPatch(rawPatch, &obj, "/Field1")) - require.Equal(t, expected, obj) - }) -} diff --git a/x/oauth2cors/cors.go b/x/oauth2cors/cors.go index efde2b8cfb2..99ef6bd3b13 100644 --- a/x/oauth2cors/cors.go +++ b/x/oauth2cors/cors.go @@ -91,6 +91,12 @@ func Middleware(reg interface { } } + // pre-flight requests do not contain credentials (cookies, HTTP authorization) + // so we return true in all cases here. + if r.Method == http.MethodOptions { + return true + } + username, _, ok := r.BasicAuth() if !ok || username == "" { token := fosite.AccessTokenFromRequest(r) diff --git a/x/oauth2cors/cors_test.go b/x/oauth2cors/cors_test.go index c9e04d2db67..83e513653df 100644 --- a/x/oauth2cors/cors_test.go +++ b/x/oauth2cors/cors_test.go @@ -52,6 +52,7 @@ func TestOAuth2AwareCORSMiddleware(t *testing.T) { code int header http.Header expectHeader http.Header + method string }{ { d: "should ignore when disabled", @@ -160,6 +161,17 @@ func TestOAuth2AwareCORSMiddleware(t *testing.T) { header: http.Header{"Origin": {"http://foobar.com"}, "Authorization": {fmt.Sprintf("Basic %s", x.BasicAuth("foo-7", "bar"))}}, expectHeader: http.Header{"Access-Control-Allow-Credentials": []string{"true"}, "Access-Control-Allow-Origin": []string{"http://foobar.com"}, "Access-Control-Expose-Headers": []string{"Content-Type"}, "Vary": []string{"Origin"}}, }, + { + d: "should succeed on pre-flight request when token introspection fails", + prep: func(t *testing.T, r driver.Registry) { + r.Config().MustSet("serve.public.cors.enabled", true) + r.Config().MustSet("serve.public.cors.allowed_origins", []string{"http://not-test-domain.com"}) + }, + code: http.StatusNotImplemented, + header: http.Header{"Origin": {"http://foobar.com"}, "Authorization": {"Bearer 1234"}}, + expectHeader: http.Header{"Access-Control-Allow-Credentials": []string{"true"}, "Access-Control-Allow-Origin": []string{"http://foobar.com"}, "Access-Control-Expose-Headers": []string{"Content-Type"}, "Vary": []string{"Origin"}}, + method: "OPTIONS", + }, { d: "should fail when token introspection fails", prep: func(t *testing.T, r driver.Registry) { @@ -237,7 +249,11 @@ func TestOAuth2AwareCORSMiddleware(t *testing.T) { tc.prep(t, r) } - req, err := http.NewRequest("GET", "http://foobar.com/", nil) + method := "GET" + if tc.method != "" { + method = tc.method + } + req, err := http.NewRequest(method, "http://foobar.com/", nil) require.NoError(t, err) for k := range tc.header { req.Header.Set(k, tc.header.Get(k)) From 92fb1244a156f1f65808224ea28fb898eacaf0ea Mon Sep 17 00:00:00 2001 From: pedrule Date: Thu, 25 Aug 2022 13:59:35 +0200 Subject: [PATCH 3/5] feat(device auth grant): add entrypoint script to debug --- entrypoint.sh | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100755 entrypoint.sh diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 00000000000..5c0f50f23ba --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +FILE_CHANGE_LOG_FILE=/tmp/changes.log +SERVICE_ARGS="$@" + +echo "*******$SERVICE_ARGS*********" + +log() { + echo "***** $1 *****" +} + +init() { + log "Initializing" + truncate -s 0 ${FILE_CHANGE_LOG_FILE} + tail -f ${FILE_CHANGE_LOG_FILE} & +} + +build() { + log "Building ${SERVICE_NAME} binary" + go env -w GOPROXY="proxy.golang.org,direct" + go mod download + go build -gcflags "all=-N -l" -o /${SERVICE_NAME} +} + +start() { + log "Starting Delve" + # ./entrypoint.sh serve all -c ../hydra/config.yml --dangerous-force-http + # dlv --listen=:20001 --headless=true --api-version=2 --accept-multiclient exec /${SERVICE_NAME} -- ${SERVICE_ARGS} & + /${SERVICE_NAME} ${SERVICE_ARGS} & +} + +restart() { + build + + log "Killing old processes" + killall dlv + killall ${SERVICE_NAME} + + start +} + +watch() { + log "Watching for changes" + inotifywait -e "MODIFY,DELETE,MOVED_TO,MOVED_FROM" -m -r ${PWD} | ( + while true; do + read path action file + ext=${file: -3} + if [[ "$ext" == ".go" ]]; then + echo "$file" + fi + done + ) | ( + WAITING="" + while true; do + file="" + read -t 1 file + if test -z "$file"; then + if test ! -z "$WAITING"; then + echo "CHANGED" + WAITING="" + fi + else + log "File ${file} changed" >> ${FILE_CHANGE_LOG_FILE} + WAITING=1 + fi + done + ) | ( + while true; do + read TMP + restart + done + ) +} + +# main part +init +build +start +# watch From f720b577fc84975015c6cc5847e39e68e22b5317 Mon Sep 17 00:00:00 2001 From: pedrule Date: Thu, 25 Aug 2022 14:04:49 +0200 Subject: [PATCH 4/5] feat(device auth grant): implements the feature --- consent/handler.go | 87 +++++++ consent/helper.go | 2 +- consent/manager.go | 9 + consent/strategy.go | 1 + consent/strategy_default.go | 214 +++++++++++++++++- consent/types.go | 78 +++++++ driver/config/provider.go | 27 +++ driver/registry_base.go | 7 + internal/httpclient-next/go.mod | 4 +- oauth2/handler.go | 116 ++++++++++ oauth2/session.go | 4 + ...00000000_create_device_link_request.up.sql | 23 ++ ...0818111500000000_oauth2_device_code.up.sql | 24 ++ ...220818111500000000_oauth2_user_code.up.sql | 25 ++ persistence/sql/persister_consent.go | 32 +++ persistence/sql/persister_oauth2.go | 153 ++++++++++++- spec/config.json | 19 ++ x/fosite_storer.go | 2 + 18 files changed, 814 insertions(+), 13 deletions(-) create mode 100644 persistence/sql/migrations/20220728111500000000_create_device_link_request.up.sql create mode 100644 persistence/sql/migrations/20220818111500000000_oauth2_device_code.up.sql create mode 100644 persistence/sql/migrations/20220818111500000000_oauth2_user_code.up.sql diff --git a/consent/handler.go b/consent/handler.go index 78a5897d5da..ecbc50e57ed 100644 --- a/consent/handler.go +++ b/consent/handler.go @@ -48,6 +48,7 @@ const ( LoginPath = "/oauth2/auth/requests/login" ConsentPath = "/oauth2/auth/requests/consent" LogoutPath = "/oauth2/auth/requests/logout" + DevicePath = "/oauth2/auth/requests/device/usercode" SessionsPath = "/oauth2/auth/sessions" ) @@ -77,6 +78,7 @@ func (h *Handler) SetRoutes(admin *x.RouterAdmin) { admin.GET(LogoutPath, h.GetLogoutRequest) admin.PUT(LogoutPath+"/accept", h.AcceptLogoutRequest) admin.PUT(LogoutPath+"/reject", h.RejectLogoutRequest) + admin.POST(DevicePath+"/verify", h.VerifyDeviceAuthUserCode) } // swagger:route DELETE /oauth2/auth/sessions/consent admin revokeConsentSessions @@ -782,3 +784,88 @@ func (h *Handler) GetLogoutRequest(w http.ResponseWriter, r *http.Request, ps ht h.r.Writer().Write(w, r, request) } + +func (h *Handler) VerifyDeviceAuthUserCode(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + type payload struct { + UserCode string `json:"userCode"` + } + + var body payload + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) + return + } + + if body.UserCode == "" { + h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint(`Request body parameter 'userCode' is not defined but should have been.`))) + return + } + + // Find the User Code in the DB if it exists + userCodeSession, err := h.r.OAuth2Storage().GetUserCodeSession(r.Context(), body.UserCode, nil) + if err != nil { + if errors.Is(err, fosite.ErrNotFound) { + h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) + } + h.r.Writer().WriteError(w, r, err) + return + } + + // Check expiry of User Code + // ... + + // Check that it hasn't already been used + // ... + + // Find the Device Link Request using the Request ID of the User Code Session + deviceLinkReq, err := h.r.ConsentManager().GetDeviceLinkRequest(r.Context(), userCodeSession.GetID()) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + // Invalidate the User Code to ensure it can't be used again. + // if err := h.r.OAuth2Storage().InvalidateUserCodeSession(r.Context(), body.UserCode); err != nil { + // h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) + // return + // } + + // Should we create a HandledDeviceLinkRequest? + + // p.ID = challenge + // ar, err := h.r.ConsentManager().GetLoginRequest(r.Context(), challenge) + // if err != nil { + // h.r.Writer().WriteError(w, r, err) + // return + // } else if ar.Subject != "" && p.Subject != ar.Subject { + // h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Field 'subject' does not match subject from previous authentication."))) + // return + // } + + // if ar.Skip { + // p.Remember = true // If skip is true remember is also true to allow consecutive calls as the same user! + // p.AuthenticatedAt = ar.AuthenticatedAt + // } else { + // p.AuthenticatedAt = sqlxx.NullTime(time.Now().UTC(). + // // Rounding is important to avoid SQL time synchronization issues in e.g. MySQL! + // Truncate(time.Second)) + // ar.AuthenticatedAt = p.AuthenticatedAt + // } + // p.RequestedAt = ar.RequestedAt + + // request, err := h.r.ConsentManager().HandleLoginRequest(r.Context(), challenge, &p) + // if err != nil { + // h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) + // return + // } + + ru, err := url.Parse(deviceLinkReq.RequestURL) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + h.r.Writer().Write(w, r, &RequestHandlerResponse{ + RedirectTo: urlx.SetQuery(ru, url.Values{"link_verifier": {deviceLinkReq.Verifier}}).String(), + }) +} diff --git a/consent/helper.go b/consent/helper.go index ef76f7921f9..200ec55c811 100644 --- a/consent/helper.go +++ b/consent/helper.go @@ -33,7 +33,7 @@ import ( "github.com/ory/hydra/client" ) -func sanitizeClientFromRequest(ar fosite.AuthorizeRequester) *client.Client { +func sanitizeClientFromRequest(ar fosite.Requester) *client.Client { return sanitizeClient(ar.GetClient().(*client.Client)) } diff --git a/consent/manager.go b/consent/manager.go index f0fa286050b..b5453763a1f 100644 --- a/consent/manager.go +++ b/consent/manager.go @@ -64,6 +64,15 @@ type Manager interface { CreateForcedObfuscatedLoginSession(ctx context.Context, session *ForcedObfuscatedLoginSession) error GetForcedObfuscatedLoginSession(ctx context.Context, client, obfuscated string) (*ForcedObfuscatedLoginSession, error) + // Functions for the management of DeviceLink requests + CreateDeviceLinkRequest(ctx context.Context, req *DeviceLinkRequest) error + GetDeviceLinkRequest(ctx context.Context, challenge string) (*DeviceLinkRequest, error) + GetDeviceLinkRequestByVerifier(ctx context.Context, verifier string) (*DeviceLinkRequest, error) + // GetDeviceLinkRequestByUserCode(ctx context.Context, userCode string) (*DeviceLinkRequest, error) + // GetDeviceLinkRequestByDeviceCode(ctx context.Context, deviceCode string) (*DeviceLinkRequest, error) + // VerifyAndInvalidateDeviceLinkRequest(ctx context.Context, verifier string) (*HandledDeviceLinkRequest, error) + + ListUserAuthenticatedClientsWithFrontChannelLogout(ctx context.Context, subject, sid string) ([]client.Client, error) ListUserAuthenticatedClientsWithBackChannelLogout(ctx context.Context, subject, sid string) ([]client.Client, error) diff --git a/consent/strategy.go b/consent/strategy.go index fa2f9eebfed..6b942f1cb43 100644 --- a/consent/strategy.go +++ b/consent/strategy.go @@ -30,5 +30,6 @@ var _ Strategy = new(DefaultStrategy) type Strategy interface { HandleOAuth2AuthorizationRequest(w http.ResponseWriter, r *http.Request, req fosite.AuthorizeRequester) (*HandledConsentRequest, error) + HandleOAuth2DeviceAuthorizationRequest(w http.ResponseWriter, r *http.Request, req fosite.DeviceAuthorizeRequester) (*HandledConsentRequest, error) HandleOpenIDConnectLogout(w http.ResponseWriter, r *http.Request) (*LogoutResult, error) } diff --git a/consent/strategy_default.go b/consent/strategy_default.go index ad4e8b08076..9ea5fa2de95 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -207,7 +207,7 @@ func (s *DefaultStrategy) getSubjectFromIDTokenHint(ctx context.Context, idToken return sub, nil } -func (s *DefaultStrategy) forwardAuthenticationRequest(w http.ResponseWriter, r *http.Request, ar fosite.AuthorizeRequester, subject string, authenticatedAt time.Time, session *LoginSession) error { +func (s *DefaultStrategy) forwardAuthenticationRequest(w http.ResponseWriter, r *http.Request, ar fosite.Requester, subject string, authenticatedAt time.Time, session *LoginSession) error { if (subject != "" && authenticatedAt.IsZero()) || (subject == "" && !authenticatedAt.IsZero()) { return errorsx.WithStack(fosite.ErrServerError.WithHint("Consent strategy returned a non-empty subject with an empty auth date, or an empty subject with a non-empty auth date.")) } @@ -232,6 +232,13 @@ func (s *DefaultStrategy) forwardAuthenticationRequest(w http.ResponseWriter, r iu := s.c.OAuth2AuthURL() iu.RawQuery = r.URL.RawQuery + // Identify requester type + if _, ok := ar.(fosite.AuthorizeRequester); ok { + iu = s.c.OAuth2AuthURL() + } else if _, ok := ar.(*fosite.DeviceAuthorizeRequest); ok { + iu = s.c.OAuth2DeviceAuthURL() + } + var idTokenHintClaims jwtgo.MapClaims if idTokenHint := ar.GetRequestForm().Get("id_token_hint"); len(idTokenHint) > 0 { claims, err := s.getIDTokenHintClaims(r.Context(), idTokenHint) @@ -527,7 +534,7 @@ func (s *DefaultStrategy) requestConsent(w http.ResponseWriter, r *http.Request, return s.forwardConsentRequest(w, r, ar, authenticationSession, nil) } -func (s *DefaultStrategy) forwardConsentRequest(w http.ResponseWriter, r *http.Request, ar fosite.AuthorizeRequester, as *HandledLoginRequest, cs *HandledConsentRequest) error { +func (s *DefaultStrategy) forwardConsentRequest(w http.ResponseWriter, r *http.Request, ar fosite.Requester, as *HandledLoginRequest, cs *HandledConsentRequest) error { skip := false if cs != nil { skip = true @@ -583,7 +590,7 @@ func (s *DefaultStrategy) forwardConsentRequest(w http.ResponseWriter, r *http.R return errorsx.WithStack(ErrAbortOAuth2Request) } -func (s *DefaultStrategy) verifyConsent(w http.ResponseWriter, r *http.Request, req fosite.AuthorizeRequester, verifier string) (*HandledConsentRequest, error) { +func (s *DefaultStrategy) verifyConsent(w http.ResponseWriter, r *http.Request, req fosite.Requester, verifier string) (*HandledConsentRequest, error) { session, err := s.r.ConsentManager().VerifyAndInvalidateConsentRequest(r.Context(), verifier) if errors.Is(err, sqlcon.ErrNoRows) { return nil, errorsx.WithStack(fosite.ErrAccessDenied.WithHint("The consent verifier has already been used, has not been granted, or is invalid.")) @@ -1015,3 +1022,204 @@ func (s *DefaultStrategy) HandleOAuth2AuthorizationRequest(w http.ResponseWriter return consentSession, nil } + +func (s *DefaultStrategy) HandleOAuth2DeviceAuthorizationRequest(w http.ResponseWriter, r *http.Request, req fosite.DeviceAuthorizeRequester) (*HandledConsentRequest, error) { + + // 1 - Get the link_verifier value from the request + linkVerifier := strings.TrimSpace(req.GetRequestForm().Get("link_verifier")) + authenticationVerifier := strings.TrimSpace(req.GetRequestForm().Get("login_verifier")) + consentVerifier := strings.TrimSpace(req.GetRequestForm().Get("consent_verifier")) + + // Final stage of the device auth flow + if len(consentVerifier) > 0 { + handledConsent, err := s.verifyConsent(w, r, req, consentVerifier) + if err != nil { + return nil, err + } + + return handledConsent, nil + } + + // Authentication has been accepted + if len(authenticationVerifier) > 0 { + authSession, err := s.verifyDeviceGrantAuthentication(w, r, req, authenticationVerifier) + if err != nil { + return nil, err + } + + // Update Link Request with reference to Login Challenge + + // ok, we need to process this request and redirect to auth endpoint + return nil, s.forwardConsentRequest(w, r, req, authSession, nil) + } + + // User Code linking has been accepted + if len(linkVerifier) > 0 { + // handledLinkRequest, err := s.verifyUserCodeLink(w, r, req, linkVerifier) + // if err != nil { + // return nil, err + // } + + return nil, s.forwardAuthenticationRequest(w, r, req, "", time.Time{}, nil) + // return nil, s.requestAuthentication(w, r, req) + } + + // Generate the request URL + // reqURL := s.c.OAuth2DeviceAuthURL() + // reqURL.RawQuery = r.URL.RawQuery + + // linkRequest := &DeviceLinkRequest{ + // ID: req.GetID(), + // Verifier: strings.Replace(uuid.New(), "-", "", -1), + // RequestedScope: []string(req.GetRequestedScopes()), + // RequestedAudience: []string(req.GetRequestedAudience()), + // Client: sanitizeClient(req.GetClient().(*client.Client)), + // RequestedAt: time.Now().Truncate(time.Second).UTC(), + // OpenIDConnectContext: nil, + // RequestURL: reqURL.String(), + // } + + // // Persist the Device Link Request + // if err := s.r.ConsentManager().CreateDeviceLinkRequest(r.Context(), linkRequest); err != nil { + // return nil, errorsx.WithStack(err) + // } + + return nil, nil +} + +func (s *DefaultStrategy) verifyDeviceGrantAuthentication(w http.ResponseWriter, r *http.Request, req fosite.DeviceAuthorizeRequester, verifier string) (*HandledLoginRequest, error) { + ctx := r.Context() + session, err := s.r.ConsentManager().VerifyAndInvalidateLoginRequest(ctx, verifier) + if errors.Is(err, sqlcon.ErrNoRows) { + return nil, errorsx.WithStack(fosite.ErrAccessDenied.WithHint("The login verifier has already been used, has not been granted, or is invalid.")) + } else if err != nil { + return nil, err + } + + if session.HasError() { + session.Error.SetDefaults(loginRequestDeniedErrorName) + return nil, errorsx.WithStack(session.Error.toRFCError()) + } + + if session.RequestedAt.Add(s.c.ConsentRequestMaxAge()).Before(time.Now()) { + return nil, errorsx.WithStack(fosite.ErrRequestUnauthorized.WithHint("The login request has expired. Please try again.")) + } + + if err := validateCsrfSession(r, s.r.CookieStore(), cookieAuthenticationCSRFName, session.LoginRequest.CSRF, s.c.CookieSameSiteLegacyWorkaround(), s.c.TLS(config.PublicInterface).Enabled()); err != nil { + return nil, err + } + + if session.LoginRequest.Skip && !session.Remember { + return nil, errorsx.WithStack(fosite.ErrServerError.WithHint("The login request was previously remembered and can only be forgotten using the reject feature.")) + } + + if session.LoginRequest.Skip && session.Subject != session.LoginRequest.Subject { + // Revoke the session because there's clearly a mix up wrt the subject that's being authenticated + if err := s.revokeAuthenticationSession(w, r); err != nil { + return nil, err + } + + return nil, errorsx.WithStack(fosite.ErrServerError.WithHint("The login request is marked as remember, but the subject from the login confirmation does not match the original subject from the cookie.")) + } + + subjectIdentifier, err := s.obfuscateSubjectIdentifier(req.GetClient(), session.Subject, session.ForceSubjectIdentifier) + if err != nil { + return nil, err + } + + sessionID := session.LoginRequest.SessionID.String() + + if err := s.r.OpenIDConnectRequestValidator().ValidatePrompt(ctx, &fosite.AuthorizeRequest{ + // ResponseTypes: req.GetResponseTypes(), + // RedirectURI: req.GetRedirectURI(), + // State: req.GetState(), + // HandledResponseTypes, this can be safely ignored because it's not being used by validation + Request: fosite.Request{ + ID: req.GetID(), + RequestedAt: req.GetRequestedAt(), + Client: req.GetClient(), + RequestedAudience: req.GetRequestedAudience(), + GrantedAudience: req.GetGrantedAudience(), + RequestedScope: req.GetRequestedScopes(), + GrantedScope: req.GetGrantedScopes(), + Form: req.GetRequestForm(), + Session: &openid.DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: subjectIdentifier, + IssuedAt: time.Now().UTC(), // doesn't matter + ExpiresAt: time.Now().Add(time.Hour).UTC(), // doesn't matter + AuthTime: time.Time(session.AuthenticatedAt), + RequestedAt: session.RequestedAt, + }, + Headers: &jwt.Headers{}, + Subject: session.Subject, + }, + }, + }); errors.Is(err, fosite.ErrLoginRequired) { + // This indicates that something went wrong with checking the subject id - let's destroy the session to be safe + if err := s.revokeAuthenticationSession(w, r); err != nil { + return nil, err + } + + return nil, err + } else if err != nil { + return nil, err + } + + if session.ForceSubjectIdentifier != "" { + if err := s.r.ConsentManager().CreateForcedObfuscatedLoginSession(r.Context(), &ForcedObfuscatedLoginSession{ + Subject: session.Subject, + ClientID: req.GetClient().GetID(), + SubjectObfuscated: session.ForceSubjectIdentifier, + }); err != nil { + return nil, err + } + } + + if !session.LoginRequest.Skip { + if time.Time(session.AuthenticatedAt).IsZero() { + return nil, errorsx.WithStack(fosite.ErrServerError.WithHint("Expected the handled login request to contain a valid authenticated_at value but it was zero. This is a bug which should be reported to https://github.com/ory/hydra.")) + } + + if err := s.r.ConsentManager().ConfirmLoginSession(r.Context(), sessionID, time.Time(session.AuthenticatedAt), session.Subject, session.Remember); err != nil { + return nil, err + } + } + + if !session.Remember && !session.LoginRequest.Skip { + // If the session should not be remembered (and we're actually not skipping), than the user clearly don't + // wants us to store a cookie. So let's bust the authentication session (if one exists). + if err := s.revokeAuthenticationSession(w, r); err != nil { + return nil, err + } + } + + if !session.Remember || session.LoginRequest.Skip { + // If the user doesn't want to remember the session, we do not store a cookie. + // If login was skipped, it means an authentication cookie was present and + // we don't want to touch it (in order to preserve its original expiry date) + return session, nil + } + + // Not a skipped login and the user asked to remember its session, store a cookie + cookie, _ := s.r.CookieStore().Get(r, CookieName(s.c.TLS(config.PublicInterface).Enabled(), CookieAuthenticationName)) + cookie.Values[CookieAuthenticationSIDName] = sessionID + if session.RememberFor >= 0 { + cookie.Options.MaxAge = session.RememberFor + } + cookie.Options.HttpOnly = true + cookie.Options.SameSite = s.c.CookieSameSiteMode() + cookie.Options.Secure = s.c.TLS(config.PublicInterface).Enabled() + if err := cookie.Save(r, w); err != nil { + return nil, errorsx.WithStack(err) + } + + s.r.Logger().WithRequest(r). + WithFields(logrus.Fields{ + "cookie_name": CookieName(s.c.TLS(config.PublicInterface).Enabled(), CookieAuthenticationName), + "cookie_http_only": true, + "cookie_same_site": s.c.CookieSameSiteMode(), + "cookie_secure": s.c.TLS(config.PublicInterface).Enabled(), + }).Debug("Authentication session cookie was set.") + return session, nil +} \ No newline at end of file diff --git a/consent/types.go b/consent/types.go index 332b3db0466..1a3c63a9f4e 100644 --- a/consent/types.go +++ b/consent/types.go @@ -704,3 +704,81 @@ func NewConsentRequestSessionData() *ConsentRequestSessionData { IDToken: map[string]interface{}{}, } } + + +// Contains information on an ongoing device link request. +// +// swagger:model deviceLinkRequest +type DeviceLinkRequest struct { + // ID is the identifier ("link challenge") of the device link request. It is used to + // identify the session. + // + // required: true + ID string `json:"challenge" db:"challenge"` + + DeviceCode string `json:"device_code" db:"device_code"` + + // RequestedScope contains the OAuth 2.0 Scope requested by the OAuth 2.0 Client. + // + // required: true + RequestedScope sqlxx.StringSlicePipeDelimiter `json:"requested_scope" db:"requested_scope"` + + // RequestedScope contains the access token audience as requested by the OAuth 2.0 Client. + // + // required: true + RequestedAudience sqlxx.StringSlicePipeDelimiter `json:"requested_access_token_audience" db:"requested_at_audience"` + + // OpenIDConnectContext provides context for the (potential) OpenID Connect context. Implementation of these + // values in your app are optional but can be useful if you want to be fully compliant with the OpenID Connect spec. + OpenIDConnectContext *OpenIDConnectContext `json:"oidc_context" db:"oidc_context"` + + // Client is the OAuth 2.0 Client that initiated the request. + // + // required: true + Client *client.Client `json:"client" db:"-"` + + ClientID string `json:"-" db:"client_id"` + + // RequestURL is the original OAuth 2.0 Device Authorization URL requested by the OAuth 2.0 client. It is the URL which + // initiates the OAuth 2.0 Device Authorization Grant. + // + // required: true + RequestURL string `json:"request_url" db:"request_url"` + + // LoginChallenge is the login challenge this consent challenge belongs to. It can be used to associate + // a login and consent request in the login & consent app. + LoginChallenge sqlxx.NullString `json:"login_challenge" db:"login_challenge"` + + // If set to true means that the request was already handled. This + // can happen on form double-submit or other errors. If this is set + // we recommend redirecting the user to `request_url` to re-initiate + // the flow. + WasHandled bool `json:"-" db:"was_handled,r"` + + Verifier string `json:"-" db:"verifier"` + + // AuthenticatedAt sqlxx.NullTime `json:"-" db:"authenticated_at"` + RequestedAt time.Time `json:"-" db:"requested_at"` +} + +func (DeviceLinkRequest) TableName() string { + return "hydra_oauth2_device_link_request" +} + +func (r *DeviceLinkRequest) FindInDB(c *pop.Connection, id string) error { + return c.Select("hydra_oauth2_device_link_request.*"). + // LeftJoin("hydra_oauth2_device_link_request_handled as hr", "hydra_oauth2_device_link_request.challenge = hr.challenge"). + Find(r, id) +} + +func (r *DeviceLinkRequest) BeforeSave(_ *pop.Connection) error { + if r.Client != nil { + r.ClientID = r.Client.OutfacingID + } + return nil +} + +func (r *DeviceLinkRequest) AfterFind(c *pop.Connection) error { + r.Client = &client.Client{} + return sqlcon.HandleError(c.Where("id = ?", r.ClientID).First(r.Client)) +} diff --git a/driver/config/provider.go b/driver/config/provider.go index d96c0cb54c0..d2c3cba63eb 100644 --- a/driver/config/provider.go +++ b/driver/config/provider.go @@ -35,6 +35,7 @@ const ( KeyOAuth2ClientRegistrationURL = "webfinger.oidc_discovery.client_registration_url" KeyOAuth2TokenURL = "webfinger.oidc_discovery.token_url" // #nosec G101 KeyOAuth2AuthURL = "webfinger.oidc_discovery.auth_url" + KeyOAuth2DeviceAuthURL = "webfinger.oidc_discovery.device_auth_url" KeyJWKSURL = "webfinger.oidc_discovery.jwks_url" KeyOIDCDiscoverySupportedClaims = "webfinger.oidc_discovery.supported_claims" KeyOIDCDiscoverySupportedScope = "webfinger.oidc_discovery.supported_scope" @@ -51,6 +52,8 @@ const ( KeyRefreshTokenLifespan = "ttl.refresh_token" // #nosec G101 KeyIDTokenLifespan = "ttl.id_token" // #nosec G101 KeyAuthCodeLifespan = "ttl.auth_code" + KeyDeviceCodeLifespan = "ttl.device_code" + KeyUserCodeLifespan = "ttl.user_code" KeyScopeStrategy = "strategies.scope" KeyGetCookieSecrets = "secrets.cookie" KeyGetSystemSecret = "secrets.system" @@ -58,12 +61,14 @@ const ( KeyLoginURL = "urls.login" KeyLogoutURL = "urls.logout" KeyConsentURL = "urls.consent" + KeyDeviceLinkURL = "urls.device_link" KeyErrorURL = "urls.error" KeyPublicURL = "urls.self.public" KeyIssuerURL = "urls.self.issuer" KeyAccessTokenStrategy = "strategies.access_token" KeySubjectIdentifierAlgorithmSalt = "oidc.subject_identifiers.pairwise.salt" KeyPublicAllowDynamicRegistration = "oidc.dynamic_client_registration.enabled" + KeyDeviceAuthTokenPollingInterval = "oauth2.device_authorization.token_polling_interval" KeyPKCEEnforced = "oauth2.pkce.enforced" KeyPKCEEnforcedForPublicClients = "oauth2.pkce.enforced_for_public_clients" KeyLogLevel = "log.level" @@ -261,6 +266,23 @@ func (p *Provider) AuthCodeLifespan() time.Duration { return p.p.DurationF(KeyAuthCodeLifespan, time.Minute*10) } +func (p *Provider) DeviceCodeLifespan() time.Duration { + return p.p.DurationF(KeyDeviceCodeLifespan, time.Minute*15) +} + +func (p *Provider) UserCodeLifespan() time.Duration { + return p.p.DurationF(KeyUserCodeLifespan, time.Minute*15) +} + +func (p *Provider) DeviceAuthTokenPollingInterval() time.Duration { + return p.p.DurationF(KeyDeviceAuthTokenPollingInterval, time.Second*5) +} + +func (p *Provider) DeviceAuthVerificationURI() string { + return p.p.String(KeyDeviceLinkURL) +} + + func (p *Provider) ScopeStrategy() string { return p.p.String(KeyScopeStrategy) } @@ -391,6 +413,11 @@ func (p *Provider) JWKSURL() *url.URL { return p.p.RequestURIF(KeyJWKSURL, urlx.AppendPaths(p.IssuerURL(), "/.well-known/jwks.json")) } +func (p *Provider) OAuth2DeviceAuthURL() *url.URL { + return p.p.RequestURIF(KeyOAuth2DeviceAuthURL, urlx.AppendPaths(p.PublicURL(), "/oauth2/device/auth")) +} + + func (p *Provider) TokenRefreshHookURL() *url.URL { return p.p.URIF(KeyRefreshTokenHookURL, nil) } diff --git a/driver/registry_base.go b/driver/registry_base.go index 56dc44f8c2f..77e09a7bfa0 100644 --- a/driver/registry_base.go +++ b/driver/registry_base.go @@ -288,6 +288,12 @@ func (m *RegistryBase) oAuth2Config() *compose.Config { AccessTokenLifespan: m.C.AccessTokenLifespan(), RefreshTokenLifespan: m.C.RefreshTokenLifespan(), AuthorizeCodeLifespan: m.C.AuthCodeLifespan(), + DeviceAuthorisation: compose.DeviceAuthorisationConfig{ + DeviceCodeLifespan: m.C.DeviceCodeLifespan(), + UserCodeLifespan: m.C.UserCodeLifespan(), + TokenPollingInterval: m.C.DeviceAuthTokenPollingInterval(), + VerificationURI: m.C.DeviceAuthVerificationURI(), + }, IDTokenLifespan: m.C.IDTokenLifespan(), IDTokenIssuer: m.C.IssuerURL().String(), HashCost: m.C.BCryptCost(), @@ -348,6 +354,7 @@ func (m *RegistryBase) OAuth2Provider() fosite.OAuth2Provider { compose.OAuth2AuthorizeExplicitFactory, compose.OAuth2AuthorizeImplicitFactory, compose.OAuth2ClientCredentialsGrantFactory, + compose.OAuth2DeviceAuthorizeFactory, compose.OAuth2RefreshTokenGrantFactory, compose.OpenIDConnectExplicitFactory, compose.OpenIDConnectHybridFactory, diff --git a/internal/httpclient-next/go.mod b/internal/httpclient-next/go.mod index 3586a102862..1f452a104c0 100644 --- a/internal/httpclient-next/go.mod +++ b/internal/httpclient-next/go.mod @@ -2,6 +2,4 @@ module github.com/ory/hydra-client-go go 1.13 -require ( - golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 -) +require golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 diff --git a/oauth2/handler.go b/oauth2/handler.go index 0fa913e2061..7119441f92b 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -68,6 +68,8 @@ const ( RevocationPath = "/oauth2/revoke" FlushPath = "/oauth2/flush" DeleteTokensPath = "/oauth2/tokens" // #nosec G101 + + DeviceAuthPath = "/oauth2/device/auth" ) type Handler struct { @@ -107,6 +109,9 @@ func (h *Handler) SetRoutes(admin *x.RouterAdmin, public *x.RouterPublic, corsMi public.Handler("GET", UserinfoPath, corsMiddleware(http.HandlerFunc(h.UserinfoHandler))) public.Handler("POST", UserinfoPath, corsMiddleware(http.HandlerFunc(h.UserinfoHandler))) + public.GET(DeviceAuthPath, h.DeviceAuthHandler) + public.POST(DeviceAuthPath, h.DeviceAuthHandler) + admin.POST(IntrospectPath, h.IntrospectHandler) admin.POST(FlushPath, h.FlushHandler) admin.DELETE(DeleteTokensPath, h.DeleteHandler) @@ -819,3 +824,114 @@ func (h *Handler) DeleteHandler(w http.ResponseWriter, r *http.Request, _ httpro // This function will not be called, OPTIONS request will be handled by cors // this is just a placeholder. func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) {} + +func (h *Handler) DeviceAuthHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + var ctx = r.Context() + + oauthProvider := h.r.OAuth2Provider() + + deviceAuthorizeRequest, err := oauthProvider.NewDeviceAuthorizeRequest(ctx, r) + if err != nil { + x.LogError(r, err, h.r.Logger()) + oauthProvider.WriteDeviceAuthorizeError(w, deviceAuthorizeRequest, err) + return + } + + deviceAuthorizeRequest.SetSession(NewSession("")) + + if len(strings.TrimSpace(deviceAuthorizeRequest.GetRequestForm().Get("link_verifier"))) == 0 { + // Initial Device Authorisation Request. + response, err := oauthProvider.NewDeviceAuthorizeResponse(ctx, deviceAuthorizeRequest) + if err != nil { + x.LogError(r, err, h.r.Logger()) + oauthProvider.WriteDeviceAuthorizeError(w, deviceAuthorizeRequest, err) + return + } + + // Generate the request URL + reqURL := h.c.OAuth2DeviceAuthURL() + reqURL.RawQuery = r.URL.RawQuery + + // Create a Device Link Request, referencing the Device Code generated + linkRequest := &consent.DeviceLinkRequest{ + ID: deviceAuthorizeRequest.GetID(), + DeviceCode: response.GetDeviceCode(), + Verifier: strings.Replace(uuid.New(), "-", "", -1), + RequestedScope: []string(deviceAuthorizeRequest.GetRequestedScopes()), + RequestedAudience: []string(deviceAuthorizeRequest.GetRequestedAudience()), + Client: deviceAuthorizeRequest.GetClient().(*client.Client), + RequestedAt: time.Now().Truncate(time.Second).UTC(), + OpenIDConnectContext: nil, + RequestURL: reqURL.String(), + } + + // Persist the Device Link Request + if err := h.r.ConsentManager().CreateDeviceLinkRequest(r.Context(), linkRequest); err != nil { + x.LogError(r, err, h.r.Logger()) + oauthProvider.WriteDeviceAuthorizeError(w, deviceAuthorizeRequest, err) + return + } + + oauthProvider.WriteDeviceAuthorizeResponse(w, deviceAuthorizeRequest, response) + return + } + + // This must be a follow up request, as part of the usercode/auth/consent redirection journey + + consentSession, err := h.r.ConsentStrategy().HandleOAuth2DeviceAuthorizationRequest(w, r, deviceAuthorizeRequest) + if err != nil { + if errors.Is(err, consent.ErrAbortOAuth2Request) { + x.LogAudit(r, nil, h.r.AuditLogger()) + return // do nothing + } + + x.LogError(r, err, h.r.Logger()) + oauthProvider.WriteDeviceAuthorizeError(w, deviceAuthorizeRequest, err) + return + } + + if consentSession != nil { + // User has completed consent, time to update the device code entity + // Update Link Request with Consent + linkReq, err := h.r.ConsentManager().GetDeviceLinkRequestByVerifier(r.Context(), strings.TrimSpace(deviceAuthorizeRequest.GetRequestForm().Get("link_verifier"))) + if err != nil { + x.LogError(r, err, h.r.Logger()) + oauthProvider.WriteDeviceAuthorizeError(w, deviceAuthorizeRequest, err) + return + } + + deviceCodeSession, err := h.r.OAuth2Storage().GetDeviceCodeSessionByRequestID(r.Context(), linkReq.ID, deviceAuthorizeRequest.GetSession()) + if err != nil { + x.LogError(r, err, h.r.Logger()) + oauthProvider.WriteDeviceAuthorizeError(w, deviceAuthorizeRequest, err) + return + } + + rr, ok := deviceCodeSession.GetSession().(*Session) + if !ok { + // do something with the error + } + + rr.ConsentChallenge = consentSession.ID + deviceCodeSession.SetSession(rr) + + for _, scope := range consentSession.GrantedScope { + deviceCodeSession.GrantScope(scope) + } + + for _, audience := range consentSession.GrantedAudience { + deviceCodeSession.GrantAudience(audience) + } + + err = h.r.OAuth2Provider().AuthorizeDeviceCode(r.Context(), linkReq.DeviceCode, deviceCodeSession) + if err != nil { + x.LogError(r, err, h.r.Logger()) + oauthProvider.WriteDeviceAuthorizeError(w, deviceAuthorizeRequest, err) + return + } + + } + + http.Redirect(w, r, "https://www.google.com", http.StatusFound) +} + diff --git a/oauth2/session.go b/oauth2/session.go index 979fe0a4f8f..059f10696b5 100644 --- a/oauth2/session.go +++ b/oauth2/session.go @@ -124,3 +124,7 @@ func (s *Session) Clone() fosite.Session { return deepcopy.Copy(s).(fosite.Session) } + +func (s *Session) GetConsentChallenge() string { + return s.ConsentChallenge +} diff --git a/persistence/sql/migrations/20220728111500000000_create_device_link_request.up.sql b/persistence/sql/migrations/20220728111500000000_create_device_link_request.up.sql new file mode 100644 index 00000000000..609b03dd94d --- /dev/null +++ b/persistence/sql/migrations/20220728111500000000_create_device_link_request.up.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_link_request +( + challenge character varying(40) COLLATE pg_catalog."default" NOT NULL, + verifier character varying(40) COLLATE pg_catalog."default" NOT NULL, + device_code character varying(255) COLLATE pg_catalog."default" NOT NULL, + requested_scope text COLLATE pg_catalog."default" NOT NULL, + requested_at_audience text COLLATE pg_catalog."default" DEFAULT ''::text, + oidc_context text COLLATE pg_catalog."default" NOT NULL, + client_id character varying(255) COLLATE pg_catalog."default" NOT NULL, + login_challenge character varying(40) COLLATE pg_catalog."default", + requested_at timestamp without time zone NOT NULL DEFAULT now(), + request_url text COLLATE pg_catalog."default" NOT NULL, + + CONSTRAINT hydra_oauth2_device_link_request_pkey PRIMARY KEY (challenge), + CONSTRAINT hydra_oauth2_device_link_request_client_id_fk FOREIGN KEY (client_id) + REFERENCES public.hydra_client (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE, + CONSTRAINT hydra_oauth2_device_link_request_login_challenge_fk FOREIGN KEY (login_challenge) + REFERENCES public.hydra_oauth2_authentication_request (challenge) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE SET NULL +); \ No newline at end of file diff --git a/persistence/sql/migrations/20220818111500000000_oauth2_device_code.up.sql b/persistence/sql/migrations/20220818111500000000_oauth2_device_code.up.sql new file mode 100644 index 00000000000..ce08ce5b61e --- /dev/null +++ b/persistence/sql/migrations/20220818111500000000_oauth2_device_code.up.sql @@ -0,0 +1,24 @@ +CREATE TABLE IF NOT EXISTS hydra_oauth2_device_code ( + signature character varying(255) NOT NULL PRIMARY KEY, + user_code character varying(40), + request_id character varying(40) NOT NULL, + requested_at timestamp without time zone NOT NULL DEFAULT now(), + client_id character varying(255) NOT NULL, + scope text NOT NULL, + granted_scope text NOT NULL, + form_data text NOT NULL, + session_data text NOT NULL, + subject character varying(255) NOT NULL, + active boolean NOT NULL, + requested_audience text, + granted_audience text, + challenge_id character varying(40) , + -- CONSTRAINT hydra_oauth2_code_challenge_id_fk FOREIGN KEY (challenge_id) + -- REFERENCES public.hydra_oauth2_consent_request_handled (challenge) MATCH SIMPLE + -- ON UPDATE NO ACTION + -- ON DELETE CASCADE, + CONSTRAINT hydra_oauth2_device_client_id_fk FOREIGN KEY (client_id) + REFERENCES public.hydra_client (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +) diff --git a/persistence/sql/migrations/20220818111500000000_oauth2_user_code.up.sql b/persistence/sql/migrations/20220818111500000000_oauth2_user_code.up.sql new file mode 100644 index 00000000000..147ac07d8b6 --- /dev/null +++ b/persistence/sql/migrations/20220818111500000000_oauth2_user_code.up.sql @@ -0,0 +1,25 @@ +CREATE TABLE IF NOT EXISTS hydra_oauth2_user_code ( + signature character varying(255) NOT NULL PRIMARY KEY, + user_code character varying(40), + device_code character varying(40), + request_id character varying(40) NOT NULL, + requested_at timestamp without time zone NOT NULL DEFAULT now(), + client_id character varying(255) NOT NULL, + scope text NOT NULL, + granted_scope text NOT NULL, + form_data text NOT NULL, + session_data text NOT NULL, + subject character varying(255) NOT NULL, + active boolean NOT NULL, + requested_audience text, + granted_audience text, + challenge_id character varying(40) , + -- CONSTRAINT hydra_oauth2_code_challenge_id_fk FOREIGN KEY (challenge_id) + -- REFERENCES public.hydra_oauth2_consent_request_handled (challenge) MATCH SIMPLE + -- ON UPDATE NO ACTION + -- ON DELETE CASCADE, + CONSTRAINT hydra_oauth2_device_client_id_fk FOREIGN KEY (client_id) + REFERENCES public.hydra_client (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +) diff --git a/persistence/sql/persister_consent.go b/persistence/sql/persister_consent.go index 923e951fa4a..9cbe7e4bd14 100644 --- a/persistence/sql/persister_consent.go +++ b/persistence/sql/persister_consent.go @@ -532,3 +532,35 @@ func (p *Persister) FlushInactiveLoginConsentRequests(ctx context.Context, notAf return nil } + +func (p *Persister) CreateDeviceLinkRequest(ctx context.Context, req *consent.DeviceLinkRequest) error { + return errorsx.WithStack(p.Connection(ctx).Create(req)) +} + +func (p *Persister) GetDeviceLinkRequest(ctx context.Context, challenge string) (*consent.DeviceLinkRequest, error) { + var lr consent.DeviceLinkRequest + return &lr, p.transaction(ctx, func(ctx context.Context, c *pop.Connection) error { + if err := (&lr).FindInDB(c, challenge); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return errorsx.WithStack(x.ErrNotFound) + } + return sqlcon.HandleError(err) + } + + return nil + }) +} + +func (p *Persister) GetDeviceLinkRequestByVerifier(ctx context.Context, verifier string) (*consent.DeviceLinkRequest, error) { + var lr consent.DeviceLinkRequest + return &lr, p.transaction(ctx, func(ctx context.Context, c *pop.Connection) error { + if err := c.Select(lr.TableName()+".*").Where("verifier = ?", verifier).First(&lr); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return errorsx.WithStack(x.ErrNotFound) + } + return sqlcon.HandleError(err) + } + + return nil + }) +} diff --git a/persistence/sql/persister_oauth2.go b/persistence/sql/persister_oauth2.go index 2ef98f7481c..a9f3efd58f9 100644 --- a/persistence/sql/persister_oauth2.go +++ b/persistence/sql/persister_oauth2.go @@ -50,11 +50,13 @@ type ( ) const ( - sqlTableOpenID tableName = "oidc" - sqlTableAccess tableName = "access" - sqlTableRefresh tableName = "refresh" - sqlTableCode tableName = "code" - sqlTablePKCE tableName = "pkce" + sqlTableOpenID tableName = "oidc" + sqlTableAccess tableName = "access" + sqlTableRefresh tableName = "refresh" + sqlTableCode tableName = "code" + sqlTableDeviceCode tableName = "device_code" + sqlTableUserCode tableName = "user_code" + sqlTablePKCE tableName = "pkce" ) func (r OAuth2RequestSQL) TableName() string { @@ -148,6 +150,7 @@ func (r *OAuth2RequestSQL) toRequest(ctx context.Context, session fosite.Session GrantedAudience: stringsx.Splitx(r.GrantedAudience, "|"), Form: val, Session: session, + ConsentGranted: r.ConsentChallenge.Valid, }, nil } @@ -221,6 +224,21 @@ func (p *Persister) createSession(ctx context.Context, signature string, request return nil } +func (p *Persister) updateSession(ctx context.Context, signature string, requester fosite.Requester, table tableName) error { + req, err := p.sqlSchemaFromRequest(signature, requester, table) + if err != nil { + return err + } + + if err := sqlcon.HandleError(p.Connection(ctx).Update(req)); errors.Is(err, sqlcon.ErrConcurrentUpdate) { + return errors.Wrap(fosite.ErrSerializationFailure, err.Error()) + } else if err != nil { + return err + } + return nil +} + + func (p *Persister) findSessionBySignature(ctx context.Context, rawSignature string, session fosite.Session, table tableName) (fosite.Requester, error) { rawSignature = p.hashSignature(rawSignature, table) @@ -237,8 +255,14 @@ func (p *Persister) findSessionBySignature(ctx context.Context, rawSignature str fr, err = r.toRequest(ctx, session, p) if err != nil { return err - } else if table == sqlTableCode { + } + switch table { + case sqlTableCode: return errorsx.WithStack(fosite.ErrInvalidatedAuthorizeCode) + case sqlTableDeviceCode: + return errorsx.WithStack(fosite.ErrInvalidatedDeviceCode) + case sqlTableUserCode: + return errorsx.WithStack(fosite.ErrInvalidatedUserCode) } return errorsx.WithStack(fosite.ErrInactiveToken) @@ -249,6 +273,37 @@ func (p *Persister) findSessionBySignature(ctx context.Context, rawSignature str }) } +func (p *Persister) findSessionByRequestID(ctx context.Context, requestID string, session fosite.Session, table tableName) (fosite.Requester, error) { + r := OAuth2RequestSQL{Table: table} + var fr fosite.Requester + + return fr, p.transaction(ctx, func(ctx context.Context, c *pop.Connection) error { + err := p.Connection(ctx).Where("request_id = ?", requestID).First(&r) + if errors.Is(err, sql.ErrNoRows) { + return errorsx.WithStack(fosite.ErrNotFound) + } else if err != nil { + return sqlcon.HandleError(err) + } else if !r.Active { + fr, err = r.toRequest(ctx, session, p) + if err != nil { + return err + } + switch table { + case sqlTableCode: + return errorsx.WithStack(fosite.ErrInvalidatedAuthorizeCode) + case sqlTableDeviceCode: + return errorsx.WithStack(fosite.ErrInvalidatedDeviceCode) + case sqlTableUserCode: + return errorsx.WithStack(fosite.ErrInvalidatedUserCode) + } + + return errorsx.WithStack(fosite.ErrInactiveToken) + } + fr, err = r.toRequest(ctx, session, p) + return err + }) +} + func (p *Persister) deleteSessionBySignature(ctx context.Context, signature string, table tableName) error { signature = p.hashSignature(signature, table) @@ -316,6 +371,92 @@ func (p *Persister) InvalidateAuthorizeCodeSession(ctx context.Context, signatur Exec()) } +//func (p *Persister) CreateDeviceAuthorizeSession(ctx context.Context, deviceSignature string, userSignature string, request fosite.Requester) (err error) { +// req, err := p.sqlSchemaFromRequest(deviceSignature, request, sqlTableDevice) +// if err != nil { +// return err +// } + +// deviceAuthRequest := &OAuth2DeviceRequestSQL{ +// ID: req.ID, +// UserCode: userSignature, +// Request: req.Request, +// ConsentChallenge: req.ConsentChallenge, +// RequestedAt: req.RequestedAt, +// Client: req.Client, +// Scopes: req.Scopes, +// GrantedScope: req.GrantedScope, +// GrantedAudience: req.GrantedAudience, +// RequestedAudience: req.RequestedAudience, +// Form: req.Form, +// Session: req.Session, +// Subject: req.Subject, +// Active: req.Active, +// Table: req.Table, +// } + +// if err := sqlcon.HandleError(p.Connection(ctx).Create(deviceAuthRequest)); errors.Is(err, sqlcon.ErrConcurrentUpdate) { +// return errors.Wrap(fosite.ErrSerializationFailure, err.Error()) +// } else if err != nil { +// return err +// } +// return nil +// } + +// func (p *Persister) GetDeviceAuthorizeSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) { +// return p.findSessionBySignature(ctx, signature, session, sqlTableDevice) +// } + +func (p *Persister) CreateDeviceCodeSession(ctx context.Context, signature string, requester fosite.Requester) (err error) { + return p.createSession(ctx, signature, requester, sqlTableDeviceCode) +} + +func (p *Persister) UpdateDeviceCodeSession(ctx context.Context, signature string, requester fosite.Requester) (err error) { + return p.updateSession(ctx, signature, requester, sqlTableDeviceCode) +} + +func (p *Persister) GetDeviceCodeSession(ctx context.Context, signature string, session fosite.Session) (request fosite.DeviceAuthorizeRequester, err error) { + requester, err := p.findSessionBySignature(ctx, signature, session, sqlTableDeviceCode) + if err != nil { + return nil, err + } + + return &fosite.DeviceAuthorizeRequest{ + AuthorizationPending: requester.GetSession().(*oauth2.Session).ConsentChallenge == "", + Request: *requester.(*fosite.Request), + }, nil +} + +func (p *Persister) GetDeviceCodeSessionByRequestID(ctx context.Context, requestID string, session fosite.Session) (request fosite.DeviceAuthorizeRequester, err error) { + requester, err := p.findSessionByRequestID(ctx, requestID, session, sqlTableDeviceCode) + if err != nil { + return nil, err + } + + return &fosite.DeviceAuthorizeRequest{ + AuthorizationPending: requester.GetSession().(*oauth2.Session).ConsentChallenge == "", + Request: *requester.(*fosite.Request), + }, nil +} + +func (p *Persister) CreateUserCodeSession(ctx context.Context, signature string, requester fosite.Requester) (err error) { + return p.createSession(ctx, signature, requester, sqlTableUserCode) +} + +func (p *Persister) GetUserCodeSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) { + return p.findSessionBySignature(ctx, signature, session, sqlTableUserCode) +} + +func (p *Persister) InvalidateUserCodeSession(ctx context.Context, signature string) (err error) { + /* #nosec G201 table is static */ + return sqlcon.HandleError( + p.Connection(ctx). + RawQuery( + fmt.Sprintf("UPDATE %s SET active=false WHERE signature=?", OAuth2RequestSQL{Table: sqlTableUserCode}.TableName()), + signature). + Exec()) +} + func (p *Persister) CreateAccessTokenSession(ctx context.Context, signature string, requester fosite.Requester) (err error) { return p.createSession(ctx, signature, requester, sqlTableAccess) } diff --git a/spec/config.json b/spec/config.json index 7bc3a8fa8af..cbd109f8a31 100644 --- a/spec/config.json +++ b/spec/config.json @@ -623,6 +623,14 @@ "format": "uri", "examples": ["https://my-consent.app/consent"] }, + "device_link": { + "type": "string", + "description": "Sets the device link endpoint of the User Login & Consent flow when supporting the Device Authorisation Grant. Defaults to an internal fallback URL showing an error.", + "format": "uri", + "examples": [ + "https://my-device-link.app/link" + ] + }, "logout": { "type": "string", "description": "Sets the logout endpoint. Defaults to an internal fallback URL showing an error.", @@ -724,6 +732,17 @@ "type": "object", "additionalProperties": false, "properties": { + "device_authorization": { + "type": "object", + "properties": { + "token_polling_interval": { + "type": "integer", + "default": 2, + "title": "Device Authorisation Token Polling Interval", + "description": "The minimum amount of time in seconds that the client SHOULD wait between polling requests to the token endpoint." + } + } + }, "expose_internal_errors": { "type": "boolean", "description": "Set this to true if you want to share error debugging information with your OAuth 2.0 clients. Keep in mind that debug information is very valuable when dealing with errors, but might also expose database error codes and similar errors.", diff --git a/x/fosite_storer.go b/x/fosite_storer.go index 4ca3677b1cf..9074848917b 100644 --- a/x/fosite_storer.go +++ b/x/fosite_storer.go @@ -34,6 +34,8 @@ import ( type FositeStorer interface { fosite.Storage oauth2.CoreStorage + oauth2.DeviceCodeStorage + oauth2.UserCodeStorage openid.OpenIDConnectRequestStorage pkce.PKCERequestStorage rfc7523.RFC7523KeyStorage From 295a372946036e859ee2aa4adecb8dbc4a61927c Mon Sep 17 00:00:00 2001 From: pedrule Date: Thu, 1 Sep 2022 23:39:34 +0200 Subject: [PATCH 5/5] change rawquery to fix issue in query building --- consent/strategy_default.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/consent/strategy_default.go b/consent/strategy_default.go index 9ea5fa2de95..bf9c9f62562 100644 --- a/consent/strategy_default.go +++ b/consent/strategy_default.go @@ -230,14 +230,15 @@ func (s *DefaultStrategy) forwardAuthenticationRequest(w http.ResponseWriter, r // Generate the request URL iu := s.c.OAuth2AuthURL() - iu.RawQuery = r.URL.RawQuery // Identify requester type if _, ok := ar.(fosite.AuthorizeRequester); ok { iu = s.c.OAuth2AuthURL() - } else if _, ok := ar.(*fosite.DeviceAuthorizeRequest); ok { + } + if _, ok := ar.(*fosite.DeviceAuthorizeRequest); ok { iu = s.c.OAuth2DeviceAuthURL() } + iu.RawQuery = r.URL.RawQuery var idTokenHintClaims jwtgo.MapClaims if idTokenHint := ar.GetRequestForm().Get("id_token_hint"); len(idTokenHint) > 0 {