diff --git a/.docker/Dockerfile-alpine b/.docker/Dockerfile-alpine index 6f1713e00bcf..6e1848e228ed 100644 --- a/.docker/Dockerfile-alpine +++ b/.docker/Dockerfile-alpine @@ -1,4 +1,4 @@ -FROM alpine:3.18.3 +FROM alpine:3.20.0 # Because this image supports SQLite, we create /home/ory and /home/ory/sqlite which is owned by the ory user # and declare /home/ory/sqlite a volume. diff --git a/.docker/Dockerfile-build b/.docker/Dockerfile-build index a4fc5d7c936e..ef853a017273 100644 --- a/.docker/Dockerfile-build +++ b/.docker/Dockerfile-build @@ -1,6 +1,5 @@ # syntax = docker/dockerfile:1-experimental -# Workaround for https://github.com/GoogleContainerTools/distroless/issues/1342 -FROM golang:1.21 AS builder +FROM golang:1.23-bullseye AS builder RUN apt-get update && apt-get upgrade -y &&\ mkdir -p /var/lib/sqlite @@ -9,7 +8,6 @@ WORKDIR /go/src/github.com/ory/kratos COPY go.mod go.mod COPY go.sum go.sum -COPY internal/httpclient/go.* internal/httpclient/ COPY internal/client-go/go.* internal/client-go/ ENV GO111MODULE on diff --git a/.docker/Dockerfile-debug b/.docker/Dockerfile-debug index 9ed036daebe7..97a0e2b72525 100644 --- a/.docker/Dockerfile-debug +++ b/.docker/Dockerfile-debug @@ -1,4 +1,4 @@ -FROM golang:1.21 +FROM golang:1.23-bullseye ENV CGO_ENABLED 1 RUN apt-get update && apt-get install -y --no-install-recommends inotify-tools psmisc diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml index b40a9c1be15d..b3db2a2d3de2 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -1,41 +1,48 @@ # AUTO-GENERATED, DO NOT EDIT! # Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/ISSUE_TEMPLATE/BUG-REPORT.yml -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." + - label: + '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 + - label: + "I agree to follow this project's [Code of Conduct](https://github.com/ory/kratos/blob/master/CODE_OF_CONDUCT.md)." required: true - - label: "I have read and am following this repository's [Contribution + - label: + "I have read and am following this repository's [Contribution Guidelines](https://github.com/ory/kratos/blob/master/CONTRIBUTING.md)." required: true - - label: "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)." + - label: + 'I have joined the [Ory Community Slack](https://slack.ory.sh).' + - label: + 'I am signed up to the [Ory Security Patch + Newsletter](https://www.ory.sh/l/sign-up-newsletter).' id: checklist type: checkboxes - attributes: - description: "Enter the slug or API URL of the affected Ory Network project. Leave empty when you are self-hosting." - label: "Ory Network Project" - placeholder: "https://.projects.oryapis.com" + description: + 'Enter the slug or API URL of the affected Ory Network project. Leave + empty when you are self-hosting.' + label: 'Ory Network Project' + placeholder: 'https://.projects.oryapis.com' id: ory-network-project type: input - 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: @@ -49,16 +56,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 + description: + '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 .... @@ -66,10 +74,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: @@ -78,14 +86,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 Network - macOS @@ -96,19 +104,19 @@ body: id: operating-system type: dropdown - attributes: - label: "In which environment are you deploying?" + label: 'In which environment are you deploying?' options: - Ory Network - 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 36bf7935a008..a6a86f36dcc7 100644 --- a/.github/ISSUE_TEMPLATE/DESIGN-DOC.yml +++ b/.github/ISSUE_TEMPLATE/DESIGN-DOC.yml @@ -1,10 +1,11 @@ # AUTO-GENERATED, DO NOT EDIT! # Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/ISSUE_TEMPLATE/DESIGN-DOC.yml -description: "A design document is needed for non-trivial changes to the code base." +description: + 'A design document is needed for non-trivial changes to the code base.' labels: - rfc -name: "Design Document" +name: 'Design Document' body: - attributes: value: | @@ -20,32 +21,39 @@ 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." + - label: + '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 + - label: + "I agree to follow this project's [Code of Conduct](https://github.com/ory/kratos/blob/master/CODE_OF_CONDUCT.md)." required: true - - label: "I have read and am following this repository's [Contribution + - label: + "I have read and am following this repository's [Contribution Guidelines](https://github.com/ory/kratos/blob/master/CONTRIBUTING.md)." required: true - - label: "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)." + - label: + 'I have joined the [Ory Community Slack](https://slack.ory.sh).' + - label: + 'I am signed up to the [Ory Security Patch + Newsletter](https://www.ory.sh/l/sign-up-newsletter).' id: checklist type: checkboxes - attributes: - description: "Enter the slug or API URL of the affected Ory Network project. Leave empty when you are self-hosting." - label: "Ory Network Project" - placeholder: "https://.projects.oryapis.com" + description: + 'Enter the slug or API URL of the affected Ory Network project. Leave + empty when you are self-hosting.' + label: 'Ory Network Project' + placeholder: 'https://.projects.oryapis.com' id: ory-network-project type: input - 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: @@ -54,7 +62,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: @@ -66,7 +74,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 at hand in an appropriate manner. Because of this, there is no explicit guidance on how to actually describe the design. - label: "The design" + label: 'The design' id: design type: textarea validations: @@ -75,21 +83,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 feasibility of the design. - label: "Code and pseudo-code" + label: 'Code and pseudo-code' id: pseudocode type: textarea @@ -102,7 +110,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 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 5e203aacfce9..7c023c2f48b3 100644 --- a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml +++ b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml @@ -1,10 +1,11 @@ # AUTO-GENERATED, DO NOT EDIT! # Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml -description: "Suggest an idea for this project without a plan for implementation" +description: + 'Suggest an idea for this project without a plan for implementation' labels: - feat -name: "Feature Request" +name: 'Feature Request' body: - attributes: value: | @@ -13,31 +14,39 @@ 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." + - label: + '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 + - label: + "I agree to follow this project's [Code of Conduct](https://github.com/ory/kratos/blob/master/CODE_OF_CONDUCT.md)." required: true - - label: "I have read and am following this repository's [Contribution + - label: + "I have read and am following this repository's [Contribution Guidelines](https://github.com/ory/kratos/blob/master/CONTRIBUTING.md)." required: true - - label: "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)." + - label: + 'I have joined the [Ory Community Slack](https://slack.ory.sh).' + - label: + 'I am signed up to the [Ory Security Patch + Newsletter](https://www.ory.sh/l/sign-up-newsletter).' id: checklist type: checkboxes - attributes: - description: "Enter the slug or API URL of the affected Ory Network project. Leave empty when you are self-hosting." - label: "Ory Network Project" - placeholder: "https://.projects.oryapis.com" + description: + 'Enter the slug or API URL of the affected Ory Network project. Leave + empty when you are self-hosting.' + label: 'Ory Network Project' + placeholder: 'https://.projects.oryapis.com' id: ory-network-project type: input - attributes: - description: "Is your feature request related to a problem? Please describe." - label: "Describe your problem" + description: + '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,27 +59,28 @@ 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 validations: required: true - attributes: - description: "Add any other context or screenshots about the feature request here." + description: + 'Add any other context or screenshots about the feature request here.' label: Additional Context id: additional type: textarea diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index ef4c482ae405..abb0b696c9d9 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -5,8 +5,10 @@ blank_issues_enabled: false contact_links: - name: Ory Kratos Forum url: https://github.com/ory/kratos/discussions - about: Please ask and answer questions here, show your implementations and + about: + Please ask and answer questions here, show your implementations and discuss ideas. - name: Ory Chat url: https://www.ory.sh/chat - about: Hang out with other Ory community members to ask and answer questions. + about: + Hang out with other Ory community members to ask and answer questions. diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6de5d575f98e..d71e06cd0bdc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -28,7 +28,7 @@ jobs: - sdk-generate services: postgres: - image: postgres:11.8 + image: postgres:14 env: POSTGRES_DB: postgres POSTGRES_PASSWORD: test @@ -79,7 +79,7 @@ jobs: fetch-depth: 2 - uses: actions/setup-go@v4 with: - go-version: "1.21" + go-version: "1.23" - run: go list -json > go.list - name: Run nancy uses: sonatype-nexus-community/nancy-github-action@v1.0.2 @@ -88,14 +88,12 @@ jobs: - run: npm install name: Install node deps - name: Run golangci-lint - uses: golangci/golangci-lint-action@v2 + uses: golangci/golangci-lint-action@v6 env: GOGC: 100 with: args: --timeout 10m0s - version: v1.55.2 - skip-go-installation: true - skip-pkg-cache: true + version: v1.61.0 - name: Build Kratos run: make install - name: Run go-acc (tests) @@ -114,7 +112,7 @@ jobs: - sdk-generate services: postgres: - image: postgres:11.8 + image: postgres:14 env: POSTGRES_DB: postgres POSTGRES_PASSWORD: test @@ -122,7 +120,7 @@ jobs: ports: - 5432:5432 mysql: - image: mysql:5.7 + image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: test ports: @@ -152,12 +150,12 @@ jobs: name: Start CockroachDB - uses: browser-actions/setup-chrome@latest name: Install Chrome - - uses: browser-actions/setup-firefox@latest - name: Install Firefox - - uses: browser-actions/setup-geckodriver@latest - name: Install Geckodriver - with: - geckodriver-version: 0.32.0 + # - uses: browser-actions/setup-firefox@latest + # name: Install Firefox + # - uses: browser-actions/setup-geckodriver@latest + # name: Install Geckodriver + # with: + # geckodriver-version: 0.32.0 - uses: ory/ci/checkout@master with: fetch-depth: 2 @@ -172,7 +170,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v4 with: - go-version: "1.21" + go-version: "1.23" - name: Install selfservice-ui-react-native uses: actions/checkout@v3 @@ -213,9 +211,9 @@ jobs: REACT_UI_PATH: react-ui CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} - if: failure() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: - name: logs + name: cypress-${{ matrix.database }}-logs path: test/e2e/*.e2e.log test-e2e-playwright: @@ -226,7 +224,7 @@ jobs: - sdk-generate services: postgres: - image: postgres:11.8 + image: postgres:14 env: POSTGRES_DB: postgres POSTGRES_PASSWORD: test @@ -234,7 +232,7 @@ jobs: ports: - 5432:5432 mysql: - image: mysql:5.7 + image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: test ports: @@ -277,7 +275,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v4 with: - go-version: "1.21" + go-version: "1.23" - run: go build -tags sqlite,json1 . - name: Install selfservice-ui-react-native @@ -324,14 +322,14 @@ jobs: NODE_UI_PATH: node-ui REACT_UI_PATH: react-ui - if: failure() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: - name: logs + name: playwright-${{ matrix.database }}-logs path: test/e2e/*.e2e.log - if: failure() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: - name: playwright-test-results-${{ github.sha }} + name: playwright-test-results-${{ matrix.database }}-${{ github.sha }} path: | test/e2e/test-results/ test/e2e/playwright-report/ diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a4d098e9826a..1c5519d95843 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/codeql-config.yml @@ -51,7 +51,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -65,4 +65,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/cve-scan.yaml b/.github/workflows/cve-scan.yaml index 8943b520bf85..0ddfe3a915de 100644 --- a/.github/workflows/cve-scan.yaml +++ b/.github/workflows/cve-scan.yaml @@ -48,6 +48,15 @@ jobs: uses: github/codeql-action/upload-sarif@v2 with: sarif_file: ${{ steps.grype-scan.outputs.sarif }} + # - name: Kubescape scanner + # uses: kubescape/github-action@main + # id: kubescape + # with: + # verbose: true + # format: pretty-printer + # # can't whitelist CVE yet: https://github.com/kubescape/kubescape/pull/1568 + # image: oryd/kratos:${{ env.SHA_SHORT }} + # severityThreshold: critical - name: Trivy Scanner uses: aquasecurity/trivy-action@master if: ${{ always() }} @@ -80,5 +89,5 @@ jobs: shell: bash run: | echo "::group::Hadolint Scan Details" - echo "${HADOLINT_RESULTS}" | jq '.' + echo "${HADOLINT_RESULTS}" | jq '.' echo "::endgroup::" diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index ce6695943cd8..bb107819d849 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: "1.21" + go-version: "1.23" - run: make format - name: Indicate formatting issues run: git diff HEAD --exit-code --color diff --git a/.github/workflows/licenses.yml b/.github/workflows/licenses.yml index 96ce1a9f3fd9..9f5efcaaed6c 100644 --- a/.github/workflows/licenses.yml +++ b/.github/workflows/licenses.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 with: - go-version: "1.21" + go-version: "1.23" - uses: actions/setup-node@v2 with: node-version: "18" diff --git a/.gitignore b/.gitignore index d91c2fb7d111..42d798e427ad 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,8 @@ heap_profiler/ goroutine_dump/ inflight_trace_dump/ +contrib/quickstart/kratos/oidc + e2e/*.log e2e/kratos.*.yml e2e/proxy.json @@ -63,3 +65,4 @@ test/e2e/kratos.*.yml # VSCode debug artifact __debug_bin .debug.sqlite.db +.last-run.json \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 079e952252ba..81b4a23960df 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,9 +19,14 @@ linters-settings: goimports: local-prefixes: github.com/ory -run: - skip-dirs: +issues: + exclude-dirs: - sdk/ - skip-files: + exclude-files: - ".+_test.go" - "corpx/faker.go" + exclude: + - "Set is deprecated: use context-based WithConfigValue instead" + - "SetDefaultIdentitySchemaFromRaw is deprecated: Use context-based WithDefaultIdentitySchemaFromRaw instead" + - "SetDefaultIdentitySchema is deprecated: Use context-based WithDefaultIdentitySchema instead" + - "G115" diff --git a/.goreleaser.yml b/.goreleaser.yml index b45adb94eceb..dd9755c68390 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,6 +1,8 @@ +version: 2 + includes: - from_url: - url: https://raw.githubusercontent.com/ory/xgoreleaser/master/build.tmpl.yml + url: https://raw.githubusercontent.com/ory/xgoreleaser/acbadd5d1b947b046fdd0a534b132509eba8e3c8/build.tmpl.yml variables: brew_name: kratos diff --git a/.grype.yaml b/.grype.yaml index 1ba341fccad5..57438622ad00 100644 --- a/.grype.yaml +++ b/.grype.yaml @@ -1,5 +1,6 @@ #only-fixed: true ignore: + - vulnerability: GHSA-c5pj-mqfh-rvc3 # https://github.com/advisories/GHSA-c5pj-mqfh-rvc3 - vulnerability: CVE-2015-5237 - vulnerability: CVE-2022-30065 - vulnerability: CVE-2023-2650 diff --git a/.schema/openapi/gen.go.yml b/.schema/openapi/gen.go.yml index 48cee4db3b58..0ca7bcda46b9 100644 --- a/.schema/openapi/gen.go.yml +++ b/.schema/openapi/gen.go.yml @@ -4,3 +4,4 @@ generateInterfaces: true isGoSubmodule: false structPrefix: true enumClassPrefix: true +useOneOfDiscriminatorLookup: true diff --git a/.schema/openapi/patches/schema.yaml b/.schema/openapi/patches/schema.yaml index 206aceb2708e..ff661ce4079d 100644 --- a/.schema/openapi/patches/schema.yaml +++ b/.schema/openapi/patches/schema.yaml @@ -43,6 +43,7 @@ set_ory_session_token: "#/components/schemas/continueWithSetOrySessionToken" show_settings_ui: "#/components/schemas/continueWithSettingsUi" show_recovery_ui: "#/components/schemas/continueWithRecoveryUi" + redirect_browser_to: "#/components/schemas/continueWithRedirectBrowserTo" - op: add path: /components/schemas/continueWith/oneOf @@ -51,3 +52,4 @@ - "$ref": "#/components/schemas/continueWithSetOrySessionToken" - "$ref": "#/components/schemas/continueWithSettingsUi" - "$ref": "#/components/schemas/continueWithRecoveryUi" + - "$ref": "#/components/schemas/continueWithRedirectBrowserTo" diff --git a/.schema/openapi/patches/selfservice.yaml b/.schema/openapi/patches/selfservice.yaml index a966cf27401e..4ca45afa2631 100644 --- a/.schema/openapi/patches/selfservice.yaml +++ b/.schema/openapi/patches/selfservice.yaml @@ -18,6 +18,8 @@ - "$ref": "#/components/schemas/updateRegistrationFlowWithOidcMethod" - "$ref": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" - "$ref": "#/components/schemas/updateRegistrationFlowWithCodeMethod" + - "$ref": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod" + - "$ref": "#/components/schemas/updateRegistrationFlowWithProfileMethod" - op: add path: /components/schemas/updateRegistrationFlowBody/discriminator value: @@ -27,12 +29,18 @@ oidc: "#/components/schemas/updateRegistrationFlowWithOidcMethod" webauthn: "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" code: "#/components/schemas/updateRegistrationFlowWithCodeMethod" + passkey: "#/components/schemas/updateRegistrationFlowWithPasskeyMethod" + profile: "#/components/schemas/updateRegistrationFlowWithProfileMethod" - op: add - path: /components/schemas/registrationFlowState/enum + path: /components/schemas/registrationFlowState value: - - choose_method - - sent_email - - passed_challenge + title: Registration flow state (experimental) + description: The experimental state represents the state of a registration flow. This field is EXPERIMENTAL and subject to change! + type: string + enum: + - choose_method + - sent_email + - passed_challenge # end # All modifications for the login flow @@ -47,6 +55,8 @@ - "$ref": "#/components/schemas/updateLoginFlowWithWebAuthnMethod" - "$ref": "#/components/schemas/updateLoginFlowWithLookupSecretMethod" - "$ref": "#/components/schemas/updateLoginFlowWithCodeMethod" + - "$ref": "#/components/schemas/updateLoginFlowWithPasskeyMethod" + - "$ref": "#/components/schemas/updateLoginFlowWithIdentifierFirstMethod" - op: add path: /components/schemas/updateLoginFlowBody/discriminator value: @@ -58,12 +68,18 @@ webauthn: "#/components/schemas/updateLoginFlowWithWebAuthnMethod" lookup_secret: "#/components/schemas/updateLoginFlowWithLookupSecretMethod" code: "#/components/schemas/updateLoginFlowWithCodeMethod" + passkey: "#/components/schemas/updateLoginFlowWithPasskeyMethod" + identifier_first: "#/components/schemas/updateLoginFlowWithIdentifierFirstMethod" - op: add - path: /components/schemas/loginFlowState/enum + path: /components/schemas/loginFlowState value: - - choose_method - - sent_email - - passed_challenge + title: Login flow state (experimental) + description: The experimental state represents the state of a login flow. This field is EXPERIMENTAL and subject to change! + type: string + enum: + - choose_method + - sent_email + - passed_challenge # end # All modifications for the recovery flow @@ -82,11 +98,15 @@ link: "#/components/schemas/updateRecoveryFlowWithLinkMethod" code: "#/components/schemas/updateRecoveryFlowWithCodeMethod" - op: add - path: /components/schemas/recoveryFlowState/enum + path: /components/schemas/recoveryFlowState + type: string value: - - choose_method - - sent_email - - passed_challenge + title: Recovery flow state (experimental) + description: The experimental state represents the state of a recovery flow. This field is EXPERIMENTAL and subject to change! + enum: + - choose_method + - sent_email + - passed_challenge # End # All modifications for the verification flow @@ -105,11 +125,15 @@ link: "#/components/schemas/updateVerificationFlowWithLinkMethod" code: "#/components/schemas/updateVerificationFlowWithCodeMethod" - op: add - path: /components/schemas/verificationFlowState/enum + path: /components/schemas/verificationFlowState + type: string value: - - choose_method - - sent_email - - passed_challenge + title: Verification flow state (experimental) + description: The experimental state represents the state of a verification flow. This field is EXPERIMENTAL and subject to change! + enum: + - choose_method + - sent_email + - passed_challenge # End # All modifications for the settings flow @@ -121,10 +145,10 @@ - "$ref": "#/components/schemas/updateSettingsFlowWithPasswordMethod" - "$ref": "#/components/schemas/updateSettingsFlowWithProfileMethod" - "$ref": "#/components/schemas/updateSettingsFlowWithOidcMethod" - - "$ref": "#/components/schemas/updateSettingsFlowWithOidcMethod" - "$ref": "#/components/schemas/updateSettingsFlowWithTotpMethod" - "$ref": "#/components/schemas/updateSettingsFlowWithWebAuthnMethod" - "$ref": "#/components/schemas/updateSettingsFlowWithLookupMethod" + - "$ref": "#/components/schemas/updateSettingsFlowWithPasskeyMethod" - op: add path: /components/schemas/updateSettingsFlowBody/discriminator value: @@ -135,10 +159,15 @@ oidc: "#/components/schemas/updateSettingsFlowWithOidcMethod" totp: "#/components/schemas/updateSettingsFlowWithTotpMethod" webauthn: "#/components/schemas/updateSettingsFlowWithWebAuthnMethod" + passkey: "#/components/schemas/updateSettingsFlowWithPasskeyMethod" lookup_secret: "#/components/schemas/updateSettingsFlowWithLookupMethod" - op: add - path: /components/schemas/settingsFlowState/enum + path: /components/schemas/settingsFlowState value: - - show_form - - success + title: Settings flow state (experimental) + description: The experimental state represents the state of a settings flow. This field is EXPERIMENTAL and subject to change! + type: string + enum: + - show_form + - success # end diff --git a/.schema/openapi/templates/go/api.mustache b/.schema/openapi/templates/go/api.mustache index 6ee735a7ffdd..e5f00dcbfa95 100644 --- a/.schema/openapi/templates/go/api.mustache +++ b/.schema/openapi/templates/go/api.mustache @@ -321,7 +321,7 @@ func (a *{{{classname}}}Service) {{nickname}}Execute(r {{#structPrefix}}{{&class return {{#returnType}}localVarReturnValue, {{/returnType}}localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { diff --git a/.schema/version.schema.json b/.schema/version.schema.json index e04013ff2587..14192708b3c7 100644 --- a/.schema/version.schema.json +++ b/.schema/version.schema.json @@ -2,6 +2,40 @@ "$id": "https://github.com/ory/kratos/.schema/versions.config.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "oneOf": [ + { + "allOf": [ + { + "properties": { + "version": { + "const": "v1.2.0" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v1.2.0/.schemastore/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v1.1.0" + } + }, + "required": [ + "version" + ] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/kratos/v1.1.0/.schemastore/config.schema.json" + } + ] + }, { "allOf": [ { diff --git a/.schemastore/config.schema.json b/.schemastore/config.schema.json index af87ea2654b9..728502c8b1af 100644 --- a/.schemastore/config.schema.json +++ b/.schemastore/config.schema.json @@ -75,6 +75,16 @@ "additionalProperties": false, "required": ["hook"] }, + "selfServiceVerificationHook": { + "type": "object", + "properties": { + "hook": { + "const": "verification" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, "selfServiceTotpSecretsDestroyerHook": { "type": "object", "properties": { @@ -275,6 +285,13 @@ "type": "string", "description": "The HTTP method to use (GET, POST, etc)." }, + "headers": { + "type": "object", + "description": "The HTTP headers that must be applied to the Web-Hook", + "additionalProperties": { + "type": "string" + } + }, "body": { "type": "string", "oneOf": [ @@ -457,7 +474,7 @@ }, "provider": { "title": "Provider", - "description": "Can be one of github, github-app, gitlab, generic, google, microsoft, discord, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon.", + "description": "Can be one of github, github-app, gitlab, generic, google, microsoft, discord, salesforce, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon.", "type": "string", "enum": [ "github", @@ -467,6 +484,7 @@ "google", "microsoft", "discord", + "salesforce", "slack", "facebook", "auth0", @@ -478,7 +496,9 @@ "dingtalk", "patreon", "linkedin", - "lark" + "linkedin_v2", + "lark", + "x" ], "examples": ["google"] }, @@ -589,6 +609,13 @@ "enum": ["id_token", "userinfo"], "default": "id_token", "examples": ["id_token", "userinfo"] + }, + "pkce": { + "title": "Proof Key for Code Exchange", + "description": "PKCE controls if the OpenID Connect OAuth2 flow should use PKCE (Proof Key for Code Exchange). IMPORTANT: If you set this to `force`, you must whitelist a different return URL for your OAuth2 client in the provider's configuration. Instead of /self-service/methods/oidc/callback/, you must use /self-service/methods/oidc/callback", + "type": "string", + "enum": ["auto", "never", "force"], + "default": "auto" } }, "additionalProperties": false, @@ -778,6 +805,12 @@ }, { "$ref": "#/definitions/selfServiceWebHook" + }, + { + "$ref": "#/definitions/selfServiceVerificationHook" + }, + { + "$ref": "#/definitions/selfServiceShowVerificationUIHook" } ] }, @@ -875,6 +908,9 @@ "webauthn": { "$ref": "#/definitions/selfServiceAfterSettingsAuthMethod" }, + "passkey": { + "$ref": "#/definitions/selfServiceAfterSettingsAuthMethod" + }, "lookup_secret": { "$ref": "#/definitions/selfServiceAfterSettingsAuthMethod" }, @@ -911,6 +947,9 @@ "webauthn": { "$ref": "#/definitions/selfServiceAfterDefaultLoginMethod" }, + "passkey": { + "$ref": "#/definitions/selfServiceAfterDefaultLoginMethod" + }, "oidc": { "$ref": "#/definitions/selfServiceAfterOIDCLoginMethod" }, @@ -936,6 +975,12 @@ { "$ref": "#/definitions/selfServiceRequireVerifiedAddressHook" }, + { + "$ref": "#/definitions/selfServiceVerificationHook" + }, + { + "$ref": "#/definitions/selfServiceShowVerificationUIHook" + }, { "$ref": "#/definitions/b2bSSOHook" } @@ -995,6 +1040,9 @@ "webauthn": { "$ref": "#/definitions/selfServiceAfterRegistrationMethod" }, + "passkey": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, "oidc": { "$ref": "#/definitions/selfServiceAfterRegistrationMethod" }, @@ -1280,6 +1328,12 @@ }, "after": { "$ref": "#/definitions/selfServiceAfterRegistration" + }, + "enable_legacy_one_step": { + "type": "boolean", + "title": "Disable two-step registration", + "description": "Two-step registration is a significantly improved sign up flow and recommended when using more than one sign up methods. To revert to one-step registration, set this to `true`.", + "default": false } } }, @@ -1301,6 +1355,13 @@ "default": "1h", "examples": ["1h", "1m", "1s"] }, + "style": { + "title": "Login Flow Style", + "description": "The style of the login flow. If set to `unified` the login flow will be a one-step process. If set to `identifier_first` (experimental!) the login flow will first ask for the identifier and then the credentials.", + "type": "string", + "enum": ["unified", "identifier_first"], + "default": "unified" + }, "before": { "$ref": "#/definitions/selfServiceBeforeLogin" }, @@ -1425,6 +1486,48 @@ "type": "object", "additionalProperties": false, "properties": { + "b2b": { + "title": "Single Sign-On for B2B", + "description": "Single Sign-On for B2B allows your customers to bring their own (workforce) identity server (e.g. OneLogin). This feature is not available in the open source licensed code.", + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": false, + "properties": { + "organizations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the organization.", + "format": "uuid", + "examples": ["00000000-0000-0000-0000-000000000000"] + }, + "label": { + "type": "string", + "description": "The label of the organization.", + "examples": ["ACME SSO"] + }, + "domains": { + "type": "array", + "items": { + "type": "string", + "format": "hostname", + "examples": ["my-app.com"], + "description": "If this domain matches the email's domain, this provider is shown." + } + } + } + } + } + } + } + }, + "additionalProperties": false + }, "profile": { "type": "object", "additionalProperties": false, @@ -1468,24 +1571,36 @@ }, "code": { "type": "object", - "additionalProperties": false, + "additionalProperties": true, "anyOf": [ { "properties": { - "passwordless_enabled": { "const": true }, - "mfa_enabled": { "const": false } + "passwordless_enabled": { + "const": true + }, + "mfa_enabled": { + "const": false + } } }, { "properties": { - "mfa_enabled": { "const": true }, - "passwordless_enabled": { "const": false } + "mfa_enabled": { + "const": true + }, + "passwordless_enabled": { + "const": false + } } }, { "properties": { - "mfa_enabled": { "const": false }, - "passwordless_enabled": { "const": false } + "mfa_enabled": { + "const": false + }, + "passwordless_enabled": { + "const": false + } } } ], @@ -1501,12 +1616,6 @@ "title": "Enables login flows code method to fulfil MFA requests", "default": false }, - "passwordless_login_fallback_enabled": { - "type": "boolean", - "title": "Passwordless Login Fallback Enabled", - "description": "This setting allows the code method to always login a user with code if they have registered with another authentication method such as password or social sign in.", - "default": false - }, "enabled": { "type": "boolean", "title": "Enables Code Method", @@ -1523,6 +1632,13 @@ "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", "examples": ["1h", "1m", "1s"] + }, + "missing_credential_fallback_enabled": { + "type": "boolean", + "title": "Enable Code OTP as a Fallback", + "description": "Enabling this allows users to sign in with the code method, even if their identity schema or their credentials are not set up to use the code method. If enabled, a verified address (such as an email) will be used to send the code to the user. Use with caution and only if actually needed.", + + "default": false } } } @@ -1587,6 +1703,61 @@ "description": "If set to false the password validation does not check for similarity between the password and the user identifier.", "type": "boolean", "default": true + }, + "migrate_hook": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Password Migration", + "description": "If set to true will enable password migration.", + "default": false + }, + "config": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "description": "The URL the password migration hook should call", + "format": "uri" + }, + "method": { + "type": "string", + "description": "The HTTP method to use (GET, POST, etc).", + "const": "POST", + "default": "POST" + }, + "headers": { + "type": "object", + "description": "The HTTP headers that must be applied to the password migration hook.", + "additionalProperties": { + "type": "string" + } + }, + "emit_analytics_event": { + "type": "boolean", + "default": true, + "description": "Emit tracing events for this hook on delivery or error" + }, + "auth": { + "type": "object", + "title": "Auth mechanisms", + "description": "Define which auth mechanism the Web-Hook should use", + "oneOf": [ + { + "$ref": "#/definitions/webHookAuthApiKeyProperties" + }, + { + "$ref": "#/definitions/webHookAuthBasicAuthProperties" + } + ] + }, + "additionalProperties": false + } + } + } } }, "additionalProperties": false @@ -1757,6 +1928,66 @@ "required": ["config"] } }, + "passkey": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables the Passkey method", + "default": false + }, + "config": { + "type": "object", + "title": "Passkey Configuration", + "properties": { + "rp": { + "title": "Relying Party (RP) Config", + "properties": { + "display_name": { + "type": "string", + "title": "Relying Party Display Name", + "description": "A name to help the user identify this RP.", + "examples": ["Ory Foundation"] + }, + "id": { + "type": "string", + "title": "Relying Party Identifier", + "description": "The id must be a subset of the domain currently in the browser.", + "examples": ["ory.sh"] + }, + "origins": { + "type": "array", + "title": "Relying Party Origins", + "description": "A list of explicit RP origins. If left empty, this defaults to either `origin` or `id`, prepended with the current protocol schema (HTTP or HTTPS).", + "items": { + "type": "string", + "examples": [ + "https://www.ory.sh", + "https://auth.ory.sh" + ] + } + } + }, + "type": "object", + "required": ["display_name", "id"] + } + }, + "additionalProperties": false + } + }, + "if": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + }, + "then": { + "required": ["config"] + } + }, "oidc": { "type": "object", "title": "Specify OpenID Connect and OAuth2 Configuration", @@ -2021,7 +2252,6 @@ "default": "localhost" } }, - "required": ["connection_uri"], "additionalProperties": false }, "sms": { @@ -2374,7 +2604,7 @@ "additionalProperties": false }, "tracing": { - "$ref": "https://raw.githubusercontent.com/ory/x/v0.0.614/otelx/config.schema.json" + "$ref": "https://raw.githubusercontent.com/ory/x/v0.0.660/otelx/config.schema.json" }, "log": { "title": "Log", @@ -2763,6 +2993,21 @@ } } }, + "security": { + "type": "object", + "properties": { + "account_enumeration": { + "type": "object", + "properties": { + "mitigate": { + "type": "boolean", + "default": false, + "description": "Mitigate account enumeration by making it harder to figure out if an identifier (email, phone number) exists or not. Enabling this setting degrades user experience. This setting does not mitigate all possible attack vectors yet." + } + } + } + } + }, "version": { "title": "The kratos version this config is written for.", "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", @@ -2841,20 +3086,45 @@ "description": "If enabled allows Ory Sessions to be cached. Only effective in the Ory Network.", "default": false }, + "cacheable_sessions_max_age": { + "title": "Set Ory Session Edge Caching maximum age", + "description": "Set how long Ory Sessions are cached on the edge. If unset, the session expiry will be used. Only effective in the Ory Network.", + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$" + }, "use_continue_with_transitions": { "type": "boolean", "title": "Enable new flow transitions using `continue_with` items", "description": "If enabled allows new flow transitions using `continue_with` items.", "default": false + }, + "faster_session_extend": { + "type": "boolean", + "title": "Enable faster session extension", + "description": "If enabled allows faster session extension by skipping the session lookup. Disabling this feature will be deprecated in the future.", + "default": false } }, "additionalProperties": false }, "organizations": { "title": "Organizations", - "description": "Secifies which organizations are available. Only effective in the Ory Network.", + "description": "Please use selfservice.methods.b2b instead. This key will be removed. Only effective in the Ory Network.", "type": "array", "default": [] + }, + "enterprise": { + "title": "Enterprise features", + "description": "Specifies enterprise features. Only effective in the Ory Network or with a valid license.", + "type": "object", + "properties": { + "identity_schema_fallback_url_template": { + "type": "string", + "title": "Fallback URL template for identity schemas", + "description": "A fallback URL template used when looking up identity schemas." + } + }, + "additionalProperties": false } }, "allOf": [ diff --git a/.trivyignore b/.trivyignore index a142ea336cff..4a01119a556c 100644 --- a/.trivyignore +++ b/.trivyignore @@ -1,2 +1,3 @@ CVE-2022-30065 +CVE-2024-2961 CVE-2023-2650 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6142899fb09f..6e15a677be72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,354 +5,1229 @@ **Table of Contents** -- [ (2024-02-16)](#2024-02-16) - - [Bug Fixes](#bug-fixes) - - [Features](#features) - - [Tests](#tests) -- [1.1.0-pre.0 (2024-02-01)](#110-pre0-2024-02-01) +- [ (2024-09-25)](#2024-09-25) - [Breaking Changes](#breaking-changes) - - [Bug Fixes](#bug-fixes-1) + - [Features](#features) + - [Tests](#tests) +- [1.3.0-pre.0 (2024-09-25)](#130-pre0-2024-09-25) + - [Breaking Changes](#breaking-changes-1) + - [Bug Fixes](#bug-fixes) - [Code Generation](#code-generation) - [Documentation](#documentation) - [Features](#features-1) - - [Reverts](#reverts) - [Tests](#tests-1) - [Unclassified](#unclassified) -- [1.0.0 (2023-07-12)](#100-2023-07-12) - - [Bug Fixes](#bug-fixes-2) - - [Code Generation](#code-generation-1) - - [Documentation](#documentation-1) - - [Features](#features-2) - - [Tests](#tests-2) - - [Unclassified](#unclassified-1) -- [0.13.0 (2023-04-18)](#0130-2023-04-18) - - [Breaking Changes](#breaking-changes-1) - - [Bug Fixes](#bug-fixes-3) +- [1.2.0 (2024-06-05)](#120-2024-06-05) + - [Breaking Changes](#breaking-changes-2) + - [Bug Fixes](#bug-fixes-1) + - [Code Generation](#code-generation-1) + - [Documentation](#documentation-1) + - [Features](#features-2) + - [Tests](#tests-2) + - [Unclassified](#unclassified-1) +- [1.1.0 (2024-02-20)](#110-2024-02-20) + - [Breaking Changes](#breaking-changes-3) + - [Bug Fixes](#bug-fixes-2) - [Code Generation](#code-generation-2) - - [Code Refactoring](#code-refactoring) - [Documentation](#documentation-2) - [Features](#features-3) + - [Reverts](#reverts) - [Tests](#tests-3) - [Unclassified](#unclassified-2) -- [0.11.1 (2023-01-14)](#0111-2023-01-14) - - [Breaking Changes](#breaking-changes-2) +- [1.0.0 (2023-07-12)](#100-2023-07-12) + - [Bug Fixes](#bug-fixes-3) + - [Code Generation](#code-generation-3) + - [Documentation](#documentation-3) + - [Features](#features-4) + - [Tests](#tests-4) + - [Unclassified](#unclassified-3) +- [0.13.0 (2023-04-18)](#0130-2023-04-18) + - [Breaking Changes](#breaking-changes-4) - [Bug Fixes](#bug-fixes-4) - - [Code Generation](#code-generation-3) - - [Documentation](#documentation-3) - - [Features](#features-4) - - [Tests](#tests-4) -- [0.11.0 (2022-12-02)](#0110-2022-12-02) - - [Code Generation](#code-generation-4) - - [Features](#features-5) -- [0.11.0-alpha.0.pre.2 (2022-11-28)](#0110-alpha0pre2-2022-11-28) - - [Breaking Changes](#breaking-changes-3) + - [Code Generation](#code-generation-4) + - [Code Refactoring](#code-refactoring) + - [Documentation](#documentation-4) + - [Features](#features-5) + - [Tests](#tests-5) + - [Unclassified](#unclassified-4) +- [0.11.1 (2023-01-14)](#0111-2023-01-14) + - [Breaking Changes](#breaking-changes-5) - [Bug Fixes](#bug-fixes-5) - [Code Generation](#code-generation-5) - - [Code Refactoring](#code-refactoring-1) - - [Documentation](#documentation-4) + - [Documentation](#documentation-5) - [Features](#features-6) + - [Tests](#tests-6) +- [0.11.0 (2022-12-02)](#0110-2022-12-02) + - [Code Generation](#code-generation-6) + - [Features](#features-7) +- [0.11.0-alpha.0.pre.2 (2022-11-28)](#0110-alpha0pre2-2022-11-28) + - [Breaking Changes](#breaking-changes-6) + - [Bug Fixes](#bug-fixes-6) + - [Code Generation](#code-generation-7) + - [Code Refactoring](#code-refactoring-1) + - [Documentation](#documentation-6) + - [Features](#features-8) - [Reverts](#reverts-1) - - [Tests](#tests-5) - - [Unclassified](#unclassified-3) + - [Tests](#tests-7) + - [Unclassified](#unclassified-5) - [0.10.1 (2022-06-01)](#0101-2022-06-01) - - [Bug Fixes](#bug-fixes-6) - - [Code Generation](#code-generation-6) + - [Bug Fixes](#bug-fixes-7) + - [Code Generation](#code-generation-8) - [0.10.0 (2022-05-30)](#0100-2022-05-30) - - [Breaking Changes](#breaking-changes-4) - - [Bug Fixes](#bug-fixes-7) - - [Code Generation](#code-generation-7) + - [Breaking Changes](#breaking-changes-7) + - [Bug Fixes](#bug-fixes-8) + - [Code Generation](#code-generation-9) - [Code Refactoring](#code-refactoring-2) - - [Documentation](#documentation-5) - - [Features](#features-7) - - [Tests](#tests-6) - - [Unclassified](#unclassified-4) + - [Documentation](#documentation-7) + - [Features](#features-9) + - [Tests](#tests-8) + - [Unclassified](#unclassified-6) - [0.9.0-alpha.3 (2022-03-25)](#090-alpha3-2022-03-25) - - [Breaking Changes](#breaking-changes-5) - - [Bug Fixes](#bug-fixes-8) - - [Code Generation](#code-generation-8) - - [Documentation](#documentation-6) + - [Breaking Changes](#breaking-changes-8) + - [Bug Fixes](#bug-fixes-9) + - [Code Generation](#code-generation-10) + - [Documentation](#documentation-8) - [0.9.0-alpha.2 (2022-03-22)](#090-alpha2-2022-03-22) - - [Bug Fixes](#bug-fixes-9) - - [Code Generation](#code-generation-9) + - [Bug Fixes](#bug-fixes-10) + - [Code Generation](#code-generation-11) - [0.9.0-alpha.1 (2022-03-21)](#090-alpha1-2022-03-21) - - [Breaking Changes](#breaking-changes-6) - - [Bug Fixes](#bug-fixes-10) - - [Code Generation](#code-generation-10) + - [Breaking Changes](#breaking-changes-9) + - [Bug Fixes](#bug-fixes-11) + - [Code Generation](#code-generation-12) - [Code Refactoring](#code-refactoring-3) - - [Documentation](#documentation-7) - - [Features](#features-8) - - [Tests](#tests-7) - - [Unclassified](#unclassified-5) + - [Documentation](#documentation-9) + - [Features](#features-10) + - [Tests](#tests-9) + - [Unclassified](#unclassified-7) - [0.8.3-alpha.1.pre.0 (2022-01-21)](#083-alpha1pre0-2022-01-21) - - [Breaking Changes](#breaking-changes-7) - - [Bug Fixes](#bug-fixes-11) - - [Code Generation](#code-generation-11) + - [Breaking Changes](#breaking-changes-10) + - [Bug Fixes](#bug-fixes-12) + - [Code Generation](#code-generation-13) - [Code Refactoring](#code-refactoring-4) - - [Documentation](#documentation-8) - - [Features](#features-9) - - [Tests](#tests-8) + - [Documentation](#documentation-10) + - [Features](#features-11) + - [Tests](#tests-10) - [0.8.2-alpha.1 (2021-12-17)](#082-alpha1-2021-12-17) - - [Bug Fixes](#bug-fixes-12) - - [Code Generation](#code-generation-12) - - [Documentation](#documentation-9) -- [0.8.1-alpha.1 (2021-12-13)](#081-alpha1-2021-12-13) - [Bug Fixes](#bug-fixes-13) - - [Code Generation](#code-generation-13) - - [Documentation](#documentation-10) - - [Features](#features-10) - - [Tests](#tests-9) + - [Code Generation](#code-generation-14) + - [Documentation](#documentation-11) +- [0.8.1-alpha.1 (2021-12-13)](#081-alpha1-2021-12-13) + - [Bug Fixes](#bug-fixes-14) + - [Code Generation](#code-generation-15) + - [Documentation](#documentation-12) + - [Features](#features-12) + - [Tests](#tests-11) - [0.8.0-alpha.4.pre.0 (2021-11-09)](#080-alpha4pre0-2021-11-09) - - [Breaking Changes](#breaking-changes-8) - - [Bug Fixes](#bug-fixes-14) - - [Code Generation](#code-generation-14) - - [Documentation](#documentation-11) - - [Features](#features-11) - - [Tests](#tests-10) + - [Breaking Changes](#breaking-changes-11) + - [Bug Fixes](#bug-fixes-15) + - [Code Generation](#code-generation-16) + - [Documentation](#documentation-13) + - [Features](#features-13) + - [Tests](#tests-12) - [0.8.0-alpha.3 (2021-10-28)](#080-alpha3-2021-10-28) - - [Bug Fixes](#bug-fixes-15) - - [Code Generation](#code-generation-15) + - [Bug Fixes](#bug-fixes-16) + - [Code Generation](#code-generation-17) - [0.8.0-alpha.2 (2021-10-28)](#080-alpha2-2021-10-28) - - [Code Generation](#code-generation-16) + - [Code Generation](#code-generation-18) - [0.8.0-alpha.1 (2021-10-27)](#080-alpha1-2021-10-27) - - [Breaking Changes](#breaking-changes-9) - - [Bug Fixes](#bug-fixes-16) - - [Code Generation](#code-generation-17) + - [Breaking Changes](#breaking-changes-12) + - [Bug Fixes](#bug-fixes-17) + - [Code Generation](#code-generation-19) - [Code Refactoring](#code-refactoring-5) - - [Documentation](#documentation-12) - - [Features](#features-12) + - [Documentation](#documentation-14) + - [Features](#features-14) - [Reverts](#reverts-2) - - [Tests](#tests-11) - - [Unclassified](#unclassified-6) + - [Tests](#tests-13) + - [Unclassified](#unclassified-8) - [0.7.6-alpha.1 (2021-09-12)](#076-alpha1-2021-09-12) - - [Code Generation](#code-generation-18) + - [Code Generation](#code-generation-20) - [0.7.5-alpha.1 (2021-09-11)](#075-alpha1-2021-09-11) - - [Code Generation](#code-generation-19) + - [Code Generation](#code-generation-21) - [0.7.4-alpha.1 (2021-09-09)](#074-alpha1-2021-09-09) - - [Bug Fixes](#bug-fixes-17) - - [Code Generation](#code-generation-20) - - [Documentation](#documentation-13) - - [Features](#features-13) - - [Tests](#tests-12) -- [0.7.3-alpha.1 (2021-08-28)](#073-alpha1-2021-08-28) - [Bug Fixes](#bug-fixes-18) - - [Code Generation](#code-generation-21) - - [Documentation](#documentation-14) - - [Features](#features-14) -- [0.7.1-alpha.1 (2021-07-22)](#071-alpha1-2021-07-22) - - [Bug Fixes](#bug-fixes-19) - [Code Generation](#code-generation-22) - [Documentation](#documentation-15) - - [Tests](#tests-13) + - [Features](#features-15) + - [Tests](#tests-14) +- [0.7.3-alpha.1 (2021-08-28)](#073-alpha1-2021-08-28) + - [Bug Fixes](#bug-fixes-19) + - [Code Generation](#code-generation-23) + - [Documentation](#documentation-16) + - [Features](#features-16) +- [0.7.1-alpha.1 (2021-07-22)](#071-alpha1-2021-07-22) + - [Bug Fixes](#bug-fixes-20) + - [Code Generation](#code-generation-24) + - [Documentation](#documentation-17) + - [Tests](#tests-15) - [0.7.0-alpha.1 (2021-07-13)](#070-alpha1-2021-07-13) - - [Breaking Changes](#breaking-changes-10) - - [Bug Fixes](#bug-fixes-20) - - [Code Generation](#code-generation-23) + - [Breaking Changes](#breaking-changes-13) + - [Bug Fixes](#bug-fixes-21) + - [Code Generation](#code-generation-25) - [Code Refactoring](#code-refactoring-6) - - [Documentation](#documentation-16) - - [Features](#features-15) - - [Tests](#tests-14) - - [Unclassified](#unclassified-7) + - [Documentation](#documentation-18) + - [Features](#features-17) + - [Tests](#tests-16) + - [Unclassified](#unclassified-9) - [0.6.3-alpha.1 (2021-05-17)](#063-alpha1-2021-05-17) - - [Breaking Changes](#breaking-changes-11) - - [Bug Fixes](#bug-fixes-21) - - [Code Generation](#code-generation-24) + - [Breaking Changes](#breaking-changes-14) + - [Bug Fixes](#bug-fixes-22) + - [Code Generation](#code-generation-26) - [Code Refactoring](#code-refactoring-7) - [0.6.2-alpha.1 (2021-05-14)](#062-alpha1-2021-05-14) - - [Code Generation](#code-generation-25) - - [Documentation](#documentation-17) + - [Code Generation](#code-generation-27) + - [Documentation](#documentation-19) - [0.6.1-alpha.1 (2021-05-11)](#061-alpha1-2021-05-11) - - [Code Generation](#code-generation-26) - - [Features](#features-16) + - [Code Generation](#code-generation-28) + - [Features](#features-18) - [0.6.0-alpha.2 (2021-05-07)](#060-alpha2-2021-05-07) - - [Bug Fixes](#bug-fixes-22) - - [Code Generation](#code-generation-27) - - [Features](#features-17) + - [Bug Fixes](#bug-fixes-23) + - [Code Generation](#code-generation-29) + - [Features](#features-19) - [0.6.0-alpha.1 (2021-05-05)](#060-alpha1-2021-05-05) - - [Breaking Changes](#breaking-changes-12) - - [Bug Fixes](#bug-fixes-23) - - [Code Generation](#code-generation-28) + - [Breaking Changes](#breaking-changes-15) + - [Bug Fixes](#bug-fixes-24) + - [Code Generation](#code-generation-30) - [Code Refactoring](#code-refactoring-8) - - [Documentation](#documentation-18) - - [Features](#features-18) - - [Tests](#tests-15) - - [Unclassified](#unclassified-8) + - [Documentation](#documentation-20) + - [Features](#features-20) + - [Tests](#tests-17) + - [Unclassified](#unclassified-10) - [0.5.5-alpha.1 (2020-12-09)](#055-alpha1-2020-12-09) - - [Bug Fixes](#bug-fixes-24) - - [Code Generation](#code-generation-29) - - [Documentation](#documentation-19) - - [Features](#features-19) - - [Tests](#tests-16) - - [Unclassified](#unclassified-9) -- [0.5.4-alpha.1 (2020-11-11)](#054-alpha1-2020-11-11) - [Bug Fixes](#bug-fixes-25) - - [Code Generation](#code-generation-30) - - [Code Refactoring](#code-refactoring-9) - - [Documentation](#documentation-20) - - [Features](#features-20) -- [0.5.3-alpha.1 (2020-10-27)](#053-alpha1-2020-10-27) - - [Bug Fixes](#bug-fixes-26) - [Code Generation](#code-generation-31) - [Documentation](#documentation-21) - [Features](#features-21) - - [Tests](#tests-17) -- [0.5.2-alpha.1 (2020-10-22)](#052-alpha1-2020-10-22) - - [Bug Fixes](#bug-fixes-27) + - [Tests](#tests-18) + - [Unclassified](#unclassified-11) +- [0.5.4-alpha.1 (2020-11-11)](#054-alpha1-2020-11-11) + - [Bug Fixes](#bug-fixes-26) - [Code Generation](#code-generation-32) + - [Code Refactoring](#code-refactoring-9) - [Documentation](#documentation-22) - - [Tests](#tests-18) -- [0.5.1-alpha.1 (2020-10-20)](#051-alpha1-2020-10-20) - - [Bug Fixes](#bug-fixes-28) + - [Features](#features-22) +- [0.5.3-alpha.1 (2020-10-27)](#053-alpha1-2020-10-27) + - [Bug Fixes](#bug-fixes-27) - [Code Generation](#code-generation-33) - [Documentation](#documentation-23) - - [Features](#features-22) + - [Features](#features-23) - [Tests](#tests-19) - - [Unclassified](#unclassified-10) +- [0.5.2-alpha.1 (2020-10-22)](#052-alpha1-2020-10-22) + - [Bug Fixes](#bug-fixes-28) + - [Code Generation](#code-generation-34) + - [Documentation](#documentation-24) + - [Tests](#tests-20) +- [0.5.1-alpha.1 (2020-10-20)](#051-alpha1-2020-10-20) + - [Bug Fixes](#bug-fixes-29) + - [Code Generation](#code-generation-35) + - [Documentation](#documentation-25) + - [Features](#features-24) + - [Tests](#tests-21) + - [Unclassified](#unclassified-12) - [0.5.0-alpha.1 (2020-10-15)](#050-alpha1-2020-10-15) - - [Breaking Changes](#breaking-changes-13) - - [Bug Fixes](#bug-fixes-29) - - [Code Generation](#code-generation-34) + - [Breaking Changes](#breaking-changes-16) + - [Bug Fixes](#bug-fixes-30) + - [Code Generation](#code-generation-36) - [Code Refactoring](#code-refactoring-10) - - [Documentation](#documentation-24) - - [Features](#features-23) - - [Tests](#tests-20) - - [Unclassified](#unclassified-11) + - [Documentation](#documentation-26) + - [Features](#features-25) + - [Tests](#tests-22) + - [Unclassified](#unclassified-13) - [0.4.6-alpha.1 (2020-07-13)](#046-alpha1-2020-07-13) - - [Bug Fixes](#bug-fixes-30) - - [Code Generation](#code-generation-35) -- [0.4.5-alpha.1 (2020-07-13)](#045-alpha1-2020-07-13) - [Bug Fixes](#bug-fixes-31) - - [Code Generation](#code-generation-36) -- [0.4.4-alpha.1 (2020-07-10)](#044-alpha1-2020-07-10) - - [Bug Fixes](#bug-fixes-32) - [Code Generation](#code-generation-37) - - [Documentation](#documentation-25) -- [0.4.3-alpha.1 (2020-07-08)](#043-alpha1-2020-07-08) - - [Bug Fixes](#bug-fixes-33) +- [0.4.5-alpha.1 (2020-07-13)](#045-alpha1-2020-07-13) + - [Bug Fixes](#bug-fixes-32) - [Code Generation](#code-generation-38) -- [0.4.2-alpha.1 (2020-07-08)](#042-alpha1-2020-07-08) - - [Bug Fixes](#bug-fixes-34) +- [0.4.4-alpha.1 (2020-07-10)](#044-alpha1-2020-07-10) + - [Bug Fixes](#bug-fixes-33) - [Code Generation](#code-generation-39) + - [Documentation](#documentation-27) +- [0.4.3-alpha.1 (2020-07-08)](#043-alpha1-2020-07-08) + - [Bug Fixes](#bug-fixes-34) + - [Code Generation](#code-generation-40) +- [0.4.2-alpha.1 (2020-07-08)](#042-alpha1-2020-07-08) + - [Bug Fixes](#bug-fixes-35) + - [Code Generation](#code-generation-41) - [0.4.0-alpha.1 (2020-07-08)](#040-alpha1-2020-07-08) - - [Breaking Changes](#breaking-changes-14) - - [Bug Fixes](#bug-fixes-35) - - [Code Generation](#code-generation-40) + - [Breaking Changes](#breaking-changes-17) + - [Bug Fixes](#bug-fixes-36) + - [Code Generation](#code-generation-42) - [Code Refactoring](#code-refactoring-11) - - [Documentation](#documentation-26) - - [Features](#features-24) - - [Unclassified](#unclassified-12) + - [Documentation](#documentation-28) + - [Features](#features-26) + - [Unclassified](#unclassified-14) - [0.3.0-alpha.1 (2020-05-15)](#030-alpha1-2020-05-15) - - [Breaking Changes](#breaking-changes-15) - - [Bug Fixes](#bug-fixes-36) + - [Breaking Changes](#breaking-changes-18) + - [Bug Fixes](#bug-fixes-37) - [Chores](#chores) - [Code Refactoring](#code-refactoring-12) - - [Documentation](#documentation-27) - - [Features](#features-25) - - [Unclassified](#unclassified-13) + - [Documentation](#documentation-29) + - [Features](#features-27) + - [Unclassified](#unclassified-15) - [0.2.1-alpha.1 (2020-05-05)](#021-alpha1-2020-05-05) - [Chores](#chores-1) - - [Documentation](#documentation-28) + - [Documentation](#documentation-30) - [0.2.0-alpha.2 (2020-05-04)](#020-alpha2-2020-05-04) - - [Breaking Changes](#breaking-changes-16) - - [Bug Fixes](#bug-fixes-37) + - [Breaking Changes](#breaking-changes-19) + - [Bug Fixes](#bug-fixes-38) - [Chores](#chores-2) - [Code Refactoring](#code-refactoring-13) - - [Documentation](#documentation-29) - - [Features](#features-26) - - [Unclassified](#unclassified-14) + - [Documentation](#documentation-31) + - [Features](#features-28) + - [Unclassified](#unclassified-16) - [0.1.1-alpha.1 (2020-02-18)](#011-alpha1-2020-02-18) - - [Bug Fixes](#bug-fixes-38) + - [Bug Fixes](#bug-fixes-39) - [Code Refactoring](#code-refactoring-14) - - [Documentation](#documentation-30) + - [Documentation](#documentation-32) - [0.1.0-alpha.6 (2020-02-16)](#010-alpha6-2020-02-16) - - [Bug Fixes](#bug-fixes-39) + - [Bug Fixes](#bug-fixes-40) - [Code Refactoring](#code-refactoring-15) - - [Documentation](#documentation-31) - - [Features](#features-27) + - [Documentation](#documentation-33) + - [Features](#features-29) - [0.1.0-alpha.5 (2020-02-06)](#010-alpha5-2020-02-06) - - [Documentation](#documentation-32) - - [Features](#features-28) + - [Documentation](#documentation-34) + - [Features](#features-30) - [0.1.0-alpha.4 (2020-02-06)](#010-alpha4-2020-02-06) - [Continuous Integration](#continuous-integration) - - [Documentation](#documentation-33) + - [Documentation](#documentation-35) - [0.1.0-alpha.3 (2020-02-06)](#010-alpha3-2020-02-06) - [Continuous Integration](#continuous-integration-1) - [0.1.0-alpha.2 (2020-02-03)](#010-alpha2-2020-02-03) - - [Bug Fixes](#bug-fixes-40) - - [Documentation](#documentation-34) - - [Features](#features-29) - - [Unclassified](#unclassified-15) + - [Bug Fixes](#bug-fixes-41) + - [Documentation](#documentation-36) + - [Features](#features-31) + - [Unclassified](#unclassified-17) - [0.1.0-alpha.1 (2020-01-31)](#010-alpha1-2020-01-31) - - [Documentation](#documentation-35) + - [Documentation](#documentation-37) - [0.0.3-alpha.15 (2020-01-31)](#003-alpha15-2020-01-31) - - [Unclassified](#unclassified-16) + - [Unclassified](#unclassified-18) - [0.0.3-alpha.14 (2020-01-31)](#003-alpha14-2020-01-31) - - [Unclassified](#unclassified-17) + - [Unclassified](#unclassified-19) - [0.0.3-alpha.13 (2020-01-31)](#003-alpha13-2020-01-31) - - [Unclassified](#unclassified-18) + - [Unclassified](#unclassified-20) - [0.0.3-alpha.11 (2020-01-31)](#003-alpha11-2020-01-31) - - [Unclassified](#unclassified-19) + - [Unclassified](#unclassified-21) - [0.0.3-alpha.10 (2020-01-31)](#003-alpha10-2020-01-31) - - [Unclassified](#unclassified-20) + - [Unclassified](#unclassified-22) - [0.0.3-alpha.7 (2020-01-30)](#003-alpha7-2020-01-30) - - [Unclassified](#unclassified-21) + - [Unclassified](#unclassified-23) - [0.0.3-alpha.5 (2020-01-30)](#003-alpha5-2020-01-30) - [Continuous Integration](#continuous-integration-2) - - [Unclassified](#unclassified-22) + - [Unclassified](#unclassified-24) - [0.0.3-alpha.4 (2020-01-30)](#003-alpha4-2020-01-30) - - [Unclassified](#unclassified-23) + - [Unclassified](#unclassified-25) - [0.0.3-alpha.2 (2020-01-30)](#003-alpha2-2020-01-30) - - [Unclassified](#unclassified-24) + - [Unclassified](#unclassified-26) - [0.0.3-alpha.1 (2020-01-30)](#003-alpha1-2020-01-30) - - [Unclassified](#unclassified-25) + - [Unclassified](#unclassified-27) - [0.0.1-alpha.9 (2020-01-29)](#001-alpha9-2020-01-29) - [Continuous Integration](#continuous-integration-3) - [0.0.2-alpha.1 (2020-01-29)](#002-alpha1-2020-01-29) - - [Unclassified](#unclassified-26) + - [Unclassified](#unclassified-28) - [0.0.1-alpha.6 (2020-01-29)](#001-alpha6-2020-01-29) - [Continuous Integration](#continuous-integration-4) - [0.0.1-alpha.5 (2020-01-29)](#001-alpha5-2020-01-29) - [Continuous Integration](#continuous-integration-5) - - [Unclassified](#unclassified-27) + - [Unclassified](#unclassified-29) - [0.0.1-alpha.3 (2020-01-28)](#001-alpha3-2020-01-28) - [Continuous Integration](#continuous-integration-6) - - [Documentation](#documentation-36) - - [Unclassified](#unclassified-28) + - [Documentation](#documentation-38) + - [Unclassified](#unclassified-30) -# [](https://github.com/ory/kratos/compare/v1.1.0-pre.0...v) (2024-02-16) +# [](https://github.com/ory/kratos/compare/v1.3.0-pre.0...v) (2024-09-25) + +## Breaking Changes + +When using two-step registration, it was previously possible to send +`method=profile:back` to get to the previous screen. This feature was not +documented in the SDK API yet. Going forward, please instead use +`screen=previous`. + +### Features + +- Change `method=profile:back` to `screen=previous` + ([#4119](https://github.com/ory/kratos/issues/4119)) + ([2cd8483](https://github.com/ory/kratos/commit/2cd8483e809170d0524fe6a5d13837108d29fa54)) + +### Tests + +- Additional code credential test case + ([#4122](https://github.com/ory/kratos/issues/4122)) + ([4f2c854](https://github.com/ory/kratos/commit/4f2c8542ab04b88c7112d7b564d91bcfd8f5791a)) + +# [1.3.0-pre.0](https://github.com/ory/kratos/compare/v1.2.0...v1.3.0-pre.0) (2024-09-25) + +autogen: pin v1.3.0-pre.0 release commit + +## Breaking Changes + +Please note that the `via` parameter is deprecated when performing SMS 2FA. It +will be removed in a future version. If the parameter is not included in the +request, the user will see all their phone/email addresses from which to perform +the flow. + +Before upgrading, ensure that your identity schema has the appropriate code +configuration when using the code method for passwordless or 2fa login. + +If you are using the code method for 2FA login already, or you are using it for +1FA login but have not yet configured the code identifier, set +`selfservice.methods.code.config.missing_credential_fallback_enabled` to `true` +to prevent users from being locked out. + +Please note that the `via` parameter is deprecated when performing SMS 2FA. It +will be removed in a future version. If the parameter is not included in the +request, the user will see all their phone/email addresses from which to perform +the flow. + +Before upgrading, ensure that your identity schema has the appropriate code +configuration when using the code method for passwordless or 2fa login. + +If you are using the code method for 2FA login already, or you are using it for +1FA login but have not yet configured the code identifier, set +`selfservice.methods.code.config.missing_credential_fallback_enabled` to `true` +to prevent users from being locked out. + +Going forward, the `/admin/session/.../extend` endpoint will return 204 no +content for new Ory Network projects. We will deprecate returning 200 + session +body in the future. ### Bug Fixes -- Add consistency flag ([#3733](https://github.com/ory/kratos/issues/3733)) - ([fd79950](https://github.com/ory/kratos/commit/fd7995077307cc101550eda5d7724ea1f68fa98a)) -- Don't require code credential for MFA flows - ([#3753](https://github.com/ory/kratos/issues/3753)) - ([40ed809](https://github.com/ory/kratos/commit/40ed809db631149874864f216a106c43ea5df670)) -- Http courier using should use lower case json - ([#3740](https://github.com/ory/kratos/issues/3740)) - ([84149c4](https://github.com/ory/kratos/commit/84149c4b420ea89f0a16a579c017a8e7e1670204)) -- Set iss from userinfo claims if missing - ([#3744](https://github.com/ory/kratos/issues/3744)) - ([241a911](https://github.com/ory/kratos/commit/241a911af74e8ad7353d6e3cab86db20758b86fc)) +- Add continue with only for json browser requests + ([#4002](https://github.com/ory/kratos/issues/4002)) + ([e0a4010](https://github.com/ory/kratos/commit/e0a4010b84b43f364be14414a380c872b166274d)) +- Add fallback to providerLabel + ([#3999](https://github.com/ory/kratos/issues/3999)) + ([d26f204](https://github.com/ory/kratos/commit/d26f2042eb5325a8d639c08d95a005724e61cb8e)): + + This adds a fallback to the provider label when trying to register a duplicate + identifier with an oidc. + + Current error message: + + `Signing in will link your account to "test@test.com" at provider "". If you do not wish to link that account, please start a new login flow.` + + The label represents an optional label for the UI, but in my case it's always + empty. I suggest we fallback to the provider when the label is not present. In + case the label is present, the behaviour won't change. + + Fallback to provider: + + `Signing in will link your account to "test@test.com" at provider "google". If you do not wish to link that account, please start a new login flow.` + +- Add missing JS triggers + ([7597bc6](https://github.com/ory/kratos/commit/7597bc6345848b66161d5a9b7a42307bbc85c978)) +- Add PKCE config key to config schema + ([#4098](https://github.com/ory/kratos/issues/4098)) + ([2c7ff3c](https://github.com/ory/kratos/commit/2c7ff3c8baab6aaa105e2d733a483fc07537470f)) +- Batch identity created event + ([#4111](https://github.com/ory/kratos/issues/4111)) + ([340f698](https://github.com/ory/kratos/commit/340f698243bd908e217394710b475a7f686a8cf9)) +- Concurrent map update for webhook header + ([#4055](https://github.com/ory/kratos/issues/4055)) + ([6ceb2f1](https://github.com/ory/kratos/commit/6ceb2f1213e1b28d3aa72380661e4aa985bfa437)) +- Do not populate `id_first` first step for account linking flows + ([#4074](https://github.com/ory/kratos/issues/4074)) + ([6ab2637](https://github.com/ory/kratos/commit/6ab2637652013e0ff377f52355e2025d68c7b3d3)) +- Downgrade go-webauthn ([#4035](https://github.com/ory/kratos/issues/4035)) + ([4d1954a](https://github.com/ory/kratos/commit/4d1954ac74dee358f9a08e619848dfe94e4934ce)) +- Emit SelfServiceMethodUsed in SettingsSucceeded event + ([#4056](https://github.com/ory/kratos/issues/4056)) + ([76af303](https://github.com/ory/kratos/commit/76af303b20ae5dffb932169a73667a55be3f3f80)) +- Filter web hook headers ([#4048](https://github.com/ory/kratos/issues/4048)) + ([ddb838e](https://github.com/ory/kratos/commit/ddb838e0e8f7d752cd1708c505e80b6c0ccc0b8a)) +- Improve OIDC account linking UI + ([#4036](https://github.com/ory/kratos/issues/4036)) + ([2b4a618](https://github.com/ory/kratos/commit/2b4a618485c9d79762243f59b35f142083f5492c)) +- Include duplicate credentials in account linking message + ([#4079](https://github.com/ory/kratos/issues/4079)) + ([122b63d](https://github.com/ory/kratos/commit/122b63d68a3ff2ad78107300869c5a6d2aa43354)) +- Incorrect append of code credential identifier + ([#4102](https://github.com/ory/kratos/issues/4102)) + ([3215792](https://github.com/ory/kratos/commit/3215792df4cab494c05ef09e969b2fa0ed95a98b)), + closes [#4076](https://github.com/ory/kratos/issues/4076) +- Jsonnet timeouts ([#3979](https://github.com/ory/kratos/issues/3979)) + ([7c5299f](https://github.com/ory/kratos/commit/7c5299f1f832ebbe0622d0920b7a91253d26b06c)) +- Move password migration hook config + ([#3986](https://github.com/ory/kratos/issues/3986)) + ([b5a66e0](https://github.com/ory/kratos/commit/b5a66e0dde3a8fa6fdeb727482481b6302589631)): + + This moves the password migration hook to + + ```yaml + selfservice: + methods: + password: + config: + migrate_hook: ... + ``` + +- Normalize code credentials and deprecate via parameter + ([c417b4a](https://github.com/ory/kratos/commit/c417b4aa76a76d3aebb4474999d7bb072615bd9f)): + + Before this, code credentials for passwordless and mfa login were incorrectly + stored and normalized. This could cause issues where the system would not + detect the user's phone number, and where SMS/email MFA would not properly + work with the `highest_available` setting. + +- Password migration hook config + ([#4001](https://github.com/ory/kratos/issues/4001)) + ([50deedf](https://github.com/ory/kratos/commit/50deedfeecf7adbc948521371b181306a0c26cf1)): + + This fixes the config loading for the password migration hook. + +- Pw migration param ([#3998](https://github.com/ory/kratos/issues/3998)) + ([6016cc8](https://github.com/ory/kratos/commit/6016cc88a076eeea71a85d75cfb5191808b69844)) +- Refactor internal API to prevent panics + ([#4028](https://github.com/ory/kratos/issues/4028)) + ([81bc152](https://github.com/ory/kratos/commit/81bc1525f09504729c666192d458cf2eaafab99f)) +- Remove flows from log messages + ([#3913](https://github.com/ory/kratos/issues/3913)) + ([310a405](https://github.com/ory/kratos/commit/310a405202c6b44633b15ad30e1fdb8ebd153e4b)) +- Replace submit with continue button for recovery and verification and add + maxlength + ([04850f4](https://github.com/ory/kratos/commit/04850f45cfbdc89223366ffa3b540d579a3b44be)) +- Return credentials in FindByCredentialsIdentifier + ([#4068](https://github.com/ory/kratos/issues/4068)) + ([f949173](https://github.com/ory/kratos/commit/f949173b3ed3d45167bb4af8b95440d5e4a39636)): + + Instead of re-fetching the credentials later (expensive), we load them only + once. + +- Return error if invalid UUID is supplied to ids filter + ([#4116](https://github.com/ory/kratos/issues/4116)) + ([98140f2](https://github.com/ory/kratos/commit/98140f2fd43ccd889e2635e4f3e7582b92fe96ab)) +- **security:** Code credential does not respect `highest_available` setting + ([b0111d4](https://github.com/ory/kratos/commit/b0111d4bd561d0f0e2f5883f30fac36fcf7135d5)): + + This patch fixes a security vulnerability which prevents the `code` method to + properly report it's credentials count to the `highest_available` mechanism. + + For more details on this issue please refer to the + [security advisory](https://github.com/ory/kratos/security/advisories/GHSA-wc43-73w7-x2f5). + +- Timestamp precision on mysql + ([9a1f171](https://github.com/ory/kratos/commit/9a1f171c1a4a8d20dc2103073bdc11ee3fdc70af)) +- Transient_payload is lost when verification flow started as part of + registration ([#3983](https://github.com/ory/kratos/issues/3983)) + ([192f10f](https://github.com/ory/kratos/commit/192f10f4ad9eb44a612baaccfc71765d52c7e1ed)) +- Trigger oidc web hook on sign in after registration + ([#4027](https://github.com/ory/kratos/issues/4027)) + ([ad5fb09](https://github.com/ory/kratos/commit/ad5fb09687f863e7c5d45868d0b8f5ec2d965372)) +- Typo in login link CLI error messages + ([#3995](https://github.com/ory/kratos/issues/3995)) + ([8350625](https://github.com/ory/kratos/commit/835062542077b9dd8d6a30836d0455adb015265d)) +- Validate page tokens for better error codes + ([#4021](https://github.com/ory/kratos/issues/4021)) + ([32737dc](https://github.com/ory/kratos/commit/32737dc708c1ecf0ec0ceaa4bbc0ac09286186fd)) +- Whoami latency ([#4070](https://github.com/ory/kratos/issues/4070)) + ([ff6ed5b](https://github.com/ory/kratos/commit/ff6ed5b70b7f715fc38a41cedd17b5323aebd79e)) + +### Code Generation + +- Pin v1.3.0-pre.0 release commit + ([72aae5b](https://github.com/ory/kratos/commit/72aae5b625ca15552a81bcf956c96994fb716198)) + +### Documentation + +- Add google to supported providers in ID Token doc strings + ([#4026](https://github.com/ory/kratos/issues/4026)) + ([955bd8f](https://github.com/ory/kratos/commit/955bd8fbc1353d7a9f84d8f591c3af31781cf7b7)) +- Typo in changelog + ([c508980](https://github.com/ory/kratos/commit/c5089801af2a656e9c1fc371a11aeb23918ba359)) ### Features -- Add request URL to email and SMS templates - ([bf5f8c3](https://github.com/ory/kratos/commit/bf5f8c3cfb2eb523a77239addb8249adf9f8b31d)) -- Improved webhook tracing ([#3746](https://github.com/ory/kratos/issues/3746)) - ([9d7021d](https://github.com/ory/kratos/commit/9d7021d87f47690c2c1f8000e87b425e49bc9496)) -- List by OIDC cred ([#3721](https://github.com/ory/kratos/issues/3721)) - ([bff9c61](https://github.com/ory/kratos/commit/bff9c61b147648ab139e7e86cda4336b5d1cfd39)) +- Add additional messages + ([735fc5b](https://github.com/ory/kratos/commit/735fc5b2c5a99746d3012cc38ee2e1b7cc3a67f2)) +- Add browser return_to continue_with action + ([7b636d8](https://github.com/ory/kratos/commit/7b636d860c6917cb1133d6d1d7401808adb890c7)) +- Add if method to sdk + ([612e3bf](https://github.com/ory/kratos/commit/612e3bf09dbffd3feba08d5100bffbc39cbd240a)) +- Add redirect to continue_with for SPA flows + ([99c945c](https://github.com/ory/kratos/commit/99c945c92d0c2745dc8df4402d755afd53e1b9aa)): + + This patch adds the new `continue_with` action `redirect_browser_to`, which + contains the redirect URL the app should redirect to. It is only supported for + SPA (not server-side browser apps, not native apps) flows at this point in + time. + +- Add social providers to credential discovery as well + ([5f4a2bf](https://github.com/ory/kratos/commit/5f4a2bf619d540d45e96586129c8ee1e7850e745)) +- Add support for Salesforce as identity provider + ([#4003](https://github.com/ory/kratos/issues/4003)) + ([3bf1ca9](https://github.com/ory/kratos/commit/3bf1ca9030555df90ef9903c34313ae4bd1fecae)) +- Add tests for two step login + ([#3959](https://github.com/ory/kratos/issues/3959)) + ([8225e40](https://github.com/ory/kratos/commit/8225e40e3d767e945006b33eebdfc47fd242ff06)) +- Allow deletion of an individual OIDC credential + ([#3968](https://github.com/ory/kratos/issues/3968)) + ([a43cef2](https://github.com/ory/kratos/commit/a43cef23c177acddbf8b03afef087feeaca51981)): + + This extends the existing `DELETE /admin/identities/{id}/credentials/{type}` + API to accept an `?identifier=foobar` query parameter for `{type}==oidc` like + such: + + `DELETE /admin/identities/{id}/credentials/oidc?identifier=github%3A012345` + + This will delete the GitHub OIDC credential with the identifier + `github:012345` (`012345` is the subject as returned by GitHub). + + To find out which OIDC credentials exist, call + `GET /admin/identities/{id}?include_credential=oidc` beforehand. + + This will allow you to delete individual OIDC credentials for users even if + they have several set up. + +- Allow partially failing batch inserts + ([#4083](https://github.com/ory/kratos/issues/4083)) + ([4ba7033](https://github.com/ory/kratos/commit/4ba70330cf9e0eda9044b0a5a504c34493ae17ed)): + + When batch-inserting multiple identities, conflicts or validation errors of a + subset of identities in the batch still allow the rest of the identities to be + inserted. The returned JSON contains the error details that lead to the + failure. + +- Better detection if credentials exist on identifier first login + ([#3963](https://github.com/ory/kratos/issues/3963)) + ([42ade94](https://github.com/ory/kratos/commit/42ade94e32a9a7ad6c0bda785e86d7209c46d8bb)) +- Clarify session extend behavior + ([#3962](https://github.com/ory/kratos/issues/3962)) + ([af5ea35](https://github.com/ory/kratos/commit/af5ea35759e74d7a1637823abcc21dc8e3e39a9d)) +- Client-side PKCE take 3 ([#4078](https://github.com/ory/kratos/issues/4078)) + ([f7c1024](https://github.com/ory/kratos/commit/f7c102456a71b226d8353b9d59cc03fb2ba0af40)): + + - feat: client-side PKCE + + This change introduces a new configuration for OIDC providers: pkce with + values auto (default), never, force. + + When auto is specified or the field is omitted, Kratos will perform + autodiscovery and perform PKCE when the server advertises support for it. This + requires the issuer_url to be set for the provider. + + never completely disables PKCE support. This is only theoretically useful: + when a provider advertises PKCE support but doesn't actually implement it. + + force always sends a PKCE challenge in the initial redirect URL, regardless of + what the provider advertises. This setting is useful when the provider offers + PKCE but doesn't advertise it in his ./well-known/openid-configuration. + + Important: When setting pkce: force, you must whitelist a different return URL + for your OAuth2 client in the provider's configuration. Instead of + /self-service/methods/oidc/callback/, you must use + /self-service/methods/oidc/callback (note missing last path + segment). This is to enable the use of the same OAuth client ID+secret when + configuring several Kratos OIDC providers, without having to whitelist + individual redirect_uris for each Kratos provider config. + + - chore: regenerate SDK, bump DB versions, cleanup tool install + + - chore: get final organization ID from provider config during registration + and login + + - chore: fixup OIDC function signatures and improve tests + +- Emit events in identity persister + ([#4107](https://github.com/ory/kratos/issues/4107)) + ([20156f6](https://github.com/ory/kratos/commit/20156f651f2faa0a79842de8d2fb4a09ee7094c1)) +- Enable new-style OIDC state generation + ([#4121](https://github.com/ory/kratos/issues/4121)) + ([eb97243](https://github.com/ory/kratos/commit/eb97243d6499e2d9f2338a2ce3f5e39579d19086)) +- Identifier first auth + ([1bdc19a](https://github.com/ory/kratos/commit/1bdc19ae3e1a3df38234cb892f65de4a2c95f041)) +- Identifier first login for all first factor login methods + ([638b274](https://github.com/ory/kratos/commit/638b27431312bcd91844ac4a00733a840976aa4f)) +- Improve session extend performance + ([#3948](https://github.com/ory/kratos/issues/3948)) + ([4e3fad4](https://github.com/ory/kratos/commit/4e3fad4b4739b5cf00d658155350cb599f2cd06a)): + + This patch improves the performance for extending session lifespans. Lifespan + extension is tricky as it is often part of the middleware of Ory Kratos + consumers. As such, it is prone to transaction contention when we read and + write to the same session row at the same time (and potentially multiple + times). + + To address this, we: + + 1. Introduce a locking mechanism on the row to reduce transaction contention; + 2. Add a new feature flag that toggles returning 204 no content instead of + 200 + session. + + Be aware that all reads on the session table will have to wait for the + transaction to commit before they return a value. This may cause long(er) + response times on `/session/whoami` for sessions that are being extended at + the same time. + +- Password migration hook ([#3978](https://github.com/ory/kratos/issues/3978)) + ([c9d5573](https://github.com/ory/kratos/commit/c9d55730a10b71ac61bb5097f5f9c33f144f2a95)): + + This adds a password migration hook to easily migrate passwords for which we + do not have the hash. + + For each user that needs to be migrated to Ory Network, a new identity is + created with a credential of type password with a config of + {"use_password_migration_hook": true} . When a user logs in, the credential + identifier and password will be sent to the password_migration web hook if all + of these are true: The user’s identity’s password credential is + {"use_password_migration_hook": true} The password_migration hook is + configured After calling the password_migration hook, the HTTP status code + will be inspected: On 200, we parse the response as JSON and look for + {"status": "password_match"}. The password credential config will be replaced + with the hash of the actual password. On any other status code, we assume that + the password is not valid. + +- **sdk:** Add missing profile discriminator to update registration + ([0150795](https://github.com/ory/kratos/commit/0150795d902dcc7cfb2298c3b5a98da1c2541e46)) +- **sdk:** Avoid eval with javascript triggers + ([dd6e53d](https://github.com/ory/kratos/commit/dd6e53d62f343a317edf403218b20599539218c6)): + + Using `OnLoadTrigger` and `OnClickTrigger` one can now map the trigger to the + corresponding JavaScript function. + + For example, trigger `{"on_click_trigger":"oryWebAuthnRegistration"}` should + be translated to `window.oryWebAuthnRegistration()`: + + ``` + if (attrs.onClickTrigger) { + window[attrs.onClickTrigger]() + } + ``` + +- Separate 2fa refresh from 1st factor refresh + ([#3961](https://github.com/ory/kratos/issues/3961)) + ([89355d8](https://github.com/ory/kratos/commit/89355d86258ace19c03fcb38dd3861f88e28af59)) +- Set maxlength for totp input + ([51042d9](https://github.com/ory/kratos/commit/51042d99fab301f0bb44665e56c5a2364e7d8866)) ### Tests -- Fix hydra tests on master ([#3737](https://github.com/ory/kratos/issues/3737)) - ([12166b4](https://github.com/ory/kratos/commit/12166b4370d607a069f268227752bb7b18a50b57)) +- Add form hydration tests for code login + ([37781a9](https://github.com/ory/kratos/commit/37781a93dda9b8f0127217a6b0ac2434dda1cc58)) +- Add form hydration tests for idfirst login + ([633b0ba](https://github.com/ory/kratos/commit/633b0ba7f724374f4c02128a5b0f748bd2e9413e)) +- Add form hydration tests for oidc login + ([df0cdcb](https://github.com/ory/kratos/commit/df0cdcb424cae6c49143ef2ef2d0b2c95f14fffb)) +- Add form hydration tests for passkey login + ([a777854](https://github.com/ory/kratos/commit/a777854e8d99336ab8f5755fdbc9d257e5edd1c0)) +- Add form hydration tests for password login + ([7186e7e](https://github.com/ory/kratos/commit/7186e7e060e04a4918e22e0b03fefbf4eb9f4a4b)) +- Add form hydration tests for webauthn login + ([8b68163](https://github.com/ory/kratos/commit/8b68163a3f293f7dceb58397f0ef555f1d8fd7c3)) +- Add tests for idfirst + ([5f76c15](https://github.com/ory/kratos/commit/5f76c1565e89bfb99f23c3f0f3a9beadbdfa270c)) +- Deflake and parallelize persister tests + ([#3953](https://github.com/ory/kratos/issues/3953)) + ([61f87d9](https://github.com/ory/kratos/commit/61f87d90bd67e5bb1f00ee110d986e4f72fc4c91)) +- Deflake session extend config side-effect + ([#3950](https://github.com/ory/kratos/issues/3950)) + ([b192c92](https://github.com/ory/kratos/commit/b192c92d6c969d470d6479bc33dbc351d327c1f9)) +- Enable server-side config from context + ([#3954](https://github.com/ory/kratos/issues/3954)) + ([e0001b0](https://github.com/ory/kratos/commit/e0001b0db784457652581366bd7ead7cdf6b3898)) +- Improve stability of refresh test + ([#4037](https://github.com/ory/kratos/issues/4037)) + ([68693a4](https://github.com/ory/kratos/commit/68693a43e4e1e3028f17789e72d0b79f6298d139)) +- Resolve CI failures ([#4067](https://github.com/ory/kratos/issues/4067)) + ([dbf7274](https://github.com/ory/kratos/commit/dbf7274f7a4be56c33b06559875c42725bf4a351)) +- Resolve issues and update snapshots for all selfservice strategies + ([e2e81ac](https://github.com/ory/kratos/commit/e2e81ac16726b180d33c57913e3cac099daf946b)) +- Update incorrect usage of Auth0 in Salesforce tests + ([#4007](https://github.com/ory/kratos/issues/4007)) + ([6ce3068](https://github.com/ory/kratos/commit/6ce306824cec81890c50dcf23c2b8a5825f20a10)) +- Verify redirect continue_with in hook executor for browser clients + ([7b0b94d](https://github.com/ory/kratos/commit/7b0b94d30ec9069de6978427814d55a30e62adb8)) + +### Unclassified + +- Merge commit from fork + ([123e807](https://github.com/ory/kratos/commit/123e80782b392095631ee2e0d1bd6ec337c1fb79)): + + - fix(security): code credential does not respect `highest_available` setting + + This patch fixes a security vulnerability which prevents the `code` method to + properly report it's credentials count to the `highest_available` mechanism. + + For more details on this issue please refer to the + [security advisory](https://github.com/ory/kratos/security/advisories/GHSA-wc43-73w7-x2f5). + + - fix: normalize code credentials and deprecate via parameter + + Before this, code credentials for passwordless and mfa login were incorrectly + stored and normalized. This could cause issues where the system would not + detect the user's phone number, and where SMS/email MFA would not properly + work with the `highest_available` setting. + +- Update .github/workflows/ci.yaml + ([2d60772](https://github.com/ory/kratos/commit/2d60772062a684c3a27f28b8836c3548f5b8cea9)) +- Update Code QL action to v2 + ([#4008](https://github.com/ory/kratos/issues/4008)) + ([e3f1da0](https://github.com/ory/kratos/commit/e3f1da0f4bf41a8a8733758fcd9edb9910c55cfa)) + +# [1.2.0](https://github.com/ory/kratos/compare/v1.1.0...v1.2.0) (2024-06-05) + +Ory Kratos v1.2 is the most complete, scalable, and secure open-source identity +server available. We are thrilled to announce its release! + +![Ory Kratos 1.2 released](https://www.ory.sh/images/newsletter/kratos-1.2.0/banner.png) + +This release introduces two major features: two-step registration and full +PassKey with resident key support. + +Passkeys provide a secure and convenient authentication method, eliminating the +need for passwords while ensuring strong security. With this release, we have +added support for resident keys, enabling offline authentication. Credential +discovery allows users to link existing passkeys to their Ory account +seamlessly. + +[Watch the PassKey demo video](https://github.com/aeneasr/web-next-deprecated/assets/3372410/e676c518-c82a-42a6-821e-28aecadb270c) + +Two-step registration improves the user experience by dividing the registration +process into two steps. Users first enter their identity traits, and then choose +a credential method for authentication, resulting in a streamlined process. This +feature is especially useful when enabling multiple authentication strategies, +as it eliminates the need to repeat identity traits for each strategy. -# [1.1.0-pre.0](https://github.com/ory/kratos/compare/v1.0.0...v1.1.0-pre.0) (2024-02-01) +![Two-Step Registration](https://ik.imagekit.io/launchnotes/production/tr:w-1640,c-at_max,f-auto/ngul9dzfjdt3pe8benegjjeeagi1) -autogen: pin v1.1.0-pre.0 release commit +The 107 commits since v1.1 include several improvements: + +- **Webhooks** now carry session information if available. +- **Transient Payloads** are now available across all self-service flows. +- **Sign in with Twitter** is now available. +- **Sign in with LinkedIn** now includes an additional v2 provider compatible + with LinkedIn's new SSO API. +- **Two-Step Registration**: An improved registration experience that separates + entering profile information from choosing authentication methods. +- **User Credentials Meta-Information** can now be included on the list + endpoint. +- **Social Sign-In** is now resilient to double-submit issues common with + Facebook and Apple mobile login. + +**Two-Step Registration Enabled by Default**: This is now the default setting. +To disable, set `selfservice.flows.registration.enable_legacy_flow` to `true`. + +- Improved account linking and credential discovery during sign-up. +- The `return_to` parameter is now respected in OIDC API flows. +- Adjustments to database indices. +- Enhanced error messages for security violations. +- Improved SDK types. +- The `verification` and `verification_ui` hooks are now available in the login + flow. +- Webhooks now contain the correct identity state in the after-verification hook + chain. + +We are doing this survey to find out how we can support self-hosted Ory users +better. We strive to provide you with the best product and service possible and +your feedback will help us understand what we're doing well and where we can +improve to better meet your needs. We truly value your opinion and thank you in +advance for taking the time to share your thoughts with us! + +Fill out the +[survey now](https://share-eu1.hsforms.com/15DiCnJpcRuijnpAdnDhxxwextgn)! + +## Breaking Changes + +This feature enables two-step registration per default. Two-step registration is +a significantly improved sign up flow and recommended when using more than one +sign up methods. To disable two-step registration, set +`selfservice.flows.registration.enable_legacy_flow` to `true`. This value +defaults to `false`. + +### Bug Fixes + +- Add login succeeded event to post registration hook + ([#3739](https://github.com/ory/kratos/issues/3739)) + ([b685fa5](https://github.com/ory/kratos/commit/b685fa5477be2ba099fd2420b27b2411fafc7e51)) +- Add missing env vars to set up guide + ([#3855](https://github.com/ory/kratos/issues/3855)) + ([da90502](https://github.com/ory/kratos/commit/da90502dc3bf8e3d34fb4ecc531834b1919989ad)): + + Closes https://github.com/ory/kratos/issues/3828 + +- Add missing indexes and remove unused index + ([6d7372e](https://github.com/ory/kratos/commit/6d7372ee3d88ee4fc552b969dd0ff338dcc0544c)) +- Add missing indexes and remove unused index + ([#3756](https://github.com/ory/kratos/issues/3756)) + ([c905f02](https://github.com/ory/kratos/commit/c905f02473c5d77ab309a45f10251b1ba7e88584)) +- Add sms mfa via parameter to spec + ([#3766](https://github.com/ory/kratos/issues/3766)) + ([b291c95](https://github.com/ory/kratos/commit/b291c959c18c72f5edc55607ab23b4592faf8d53)) +- Allow updating just the verified_at timestamp of addresses + ([#3880](https://github.com/ory/kratos/issues/3880)) + ([696cc1b](https://github.com/ory/kratos/commit/696cc1b59b18627fec63915070f4d8c5b3e3250d)) +- Always issue session last ([#3876](https://github.com/ory/kratos/issues/3876)) + ([e942507](https://github.com/ory/kratos/commit/e94250705e999567e2ed58cebdb3f6a9d589e3ef)): + + In post persist hooks, the session issuance hook always needs to come last. + This fixes the getHooks function to ensure this. + +- Audit issues ([#3797](https://github.com/ory/kratos/issues/3797)) + ([7017490](https://github.com/ory/kratos/commit/7017490caa9c70e22d5c626773c0266521813ff5)) +- Change return urls in quickstarts + ([#3928](https://github.com/ory/kratos/issues/3928)) + ([9730e09](https://github.com/ory/kratos/commit/9730e099a656d211389d8e993c64d8082784c929)) +- Close res body ([#3870](https://github.com/ory/kratos/issues/3870)) + ([cc39f8d](https://github.com/ory/kratos/commit/cc39f8df7c235af0df616432bc4f88681896ad85)) +- CVEs in dependencies ([#3902](https://github.com/ory/kratos/issues/3902)) + ([e5d3b0a](https://github.com/ory/kratos/commit/e5d3b0afde3c80c6c9cf8815c56d82e291ede663)) +- Db index and duplicate credentials error + ([#3896](https://github.com/ory/kratos/issues/3896)) + ([9f34a21](https://github.com/ory/kratos/commit/9f34a21ea2035a5d33edd96753023a3c8c6c054c)): + + - fix: don't return password cred type if empty + - fix: better index for config.user_handle on identity_credentials + +- Do not require method to be passkey in settings schema + ([#3862](https://github.com/ory/kratos/issues/3862)) + ([660f330](https://github.com/ory/kratos/commit/660f330ab69ef0e6fd21501fbc9dfed693d4a715)) +- Don't require connection_uri in SMTP + ([#3861](https://github.com/ory/kratos/issues/3861)) + ([800f8f1](https://github.com/ory/kratos/commit/800f8f1036ef46a561d24dcdec45dd48803978d7)) +- Don't treat passkeys as AAL2 + ([#3853](https://github.com/ory/kratos/issues/3853)) + ([8eee972](https://github.com/ory/kratos/commit/8eee972d89accb02b3caa053fca2f16ed2c876f1)) +- Drop index if exists ([#3846](https://github.com/ory/kratos/issues/3846)) + ([ad0619d](https://github.com/ory/kratos/commit/ad0619d803cd2842a67c56a545ec5ab252501b0f)) +- Drop trigram index on identifiers + ([#3827](https://github.com/ory/kratos/issues/3827)) + ([8f8fd90](https://github.com/ory/kratos/commit/8f8fd90304886ecd689a85fc60c4712e47526cdd)) +- Enum type of session expandables + ([#3891](https://github.com/ory/kratos/issues/3891)) + ([63d785e](https://github.com/ory/kratos/commit/63d785e5e73ff067ec804ecc2107fac1525d3688)) +- Enum type of session expandables + ([#3895](https://github.com/ory/kratos/issues/3895)) + ([c435727](https://github.com/ory/kratos/commit/c435727c1e3c70c040b7fc7648ce621b136e5fc2)) +- Execute verification & verification_ui properly in login flows + ([#3847](https://github.com/ory/kratos/issues/3847)) + ([5aad1c1](https://github.com/ory/kratos/commit/5aad1c1e6cc92f72af56511dacb9812edb600813)) +- Ignore decrypt errors in WithDeclassifiedCredentials + ([#3731](https://github.com/ory/kratos/issues/3731)) + ([8f5192f](https://github.com/ory/kratos/commit/8f5192fbb74c4b952029a6856284de8d59027770)) +- Improve SDK discriminators + ([#3844](https://github.com/ory/kratos/issues/3844)) + ([c08b3ad](https://github.com/ory/kratos/commit/c08b3ad76c5adb712c945cdbd92a9a51832e94b9)) +- Include all creds in duplicate credential err + ([#3881](https://github.com/ory/kratos/issues/3881)) + ([e06c241](https://github.com/ory/kratos/commit/e06c241ffe3f0e696bb1cbc1d1080f9d4e09fbd2)) +- Linkedin issuer override ([#3875](https://github.com/ory/kratos/issues/3875)) + ([11d221a](https://github.com/ory/kratos/commit/11d221a4d33878930ca7025ae1b5c18b25dd1add)) +- Make sure emails can still be sent with SMS enabled + ([#3795](https://github.com/ory/kratos/issues/3795)) + ([7c68c5a](https://github.com/ory/kratos/commit/7c68c5aa69ed76a84a37a37a3555277ddc772cf8)) +- Missing indices and foreign keys + ([#3800](https://github.com/ory/kratos/issues/3800)) + ([0b32ce1](https://github.com/ory/kratos/commit/0b32ce113be47aa724d3468062ced09f8f60c52a)) +- **oidc:** Grace period for continuity container on oidc callbacks + ([#3915](https://github.com/ory/kratos/issues/3915)) + ([1a9a096](https://github.com/ory/kratos/commit/1a9a096d619925dd3718ad9dd9daf77387572ece)) +- Passing transient payloads + ([#3838](https://github.com/ory/kratos/issues/3838)) + ([d01b670](https://github.com/ory/kratos/commit/d01b6705bf36efb6e0f3d71ed22d0574ab8a98a4)) +- Prevent SMTP URL leak on unparsable URL + ([#3770](https://github.com/ory/kratos/issues/3770)) + ([c5f39f4](https://github.com/ory/kratos/commit/c5f39f4bc481e400f736ede7f8f0be546a55eebf)) +- Respect return_to in OIDC API flow error case + ([#3893](https://github.com/ory/kratos/issues/3893)) + ([e8f1bcb](https://github.com/ory/kratos/commit/e8f1bcb1342af994b8e08282aa4066ee00ffe7d4)): + + - fix: respect return_to in OIDC API flow error case + + This fix ensures that we redirect the user to the return_to URL when an error + occurs during the OIDC login for native flows. + + Native flows are initialized through the API, and the browser URL is retrieved + from a 422 response after a POST to submit the login flow. Successful OIDC + flows already returned the `code` to the `return_to` URL. Now, unsuccessful + flows return the `flow` with the current flow ID (which might have changed), + so that the caller can retrieve the full flow and act accordingly. + + - fix: ignore trivvy CVE report + + Bump in distroless is still open + +- **sdk:** Expand identity in session extension + ([#3843](https://github.com/ory/kratos/issues/3843)) + ([04f0231](https://github.com/ory/kratos/commit/04f02318d4de5290cbf100e9b301284d5ee40fe7)), + closes [#3842](https://github.com/ory/kratos/issues/3842) +- **sdk:** Improve discriminators for node and Go + ([#3821](https://github.com/ory/kratos/issues/3821)) + ([9ddf7cc](https://github.com/ory/kratos/commit/9ddf7cc7c52313c4ee13ccdc2886ad94b5d1317f)) +- Show error page on identity mismatch + ([#3790](https://github.com/ory/kratos/issues/3790)) + ([e6db689](https://github.com/ory/kratos/commit/e6db689e0de41067e6e78889c3dab9637a96236e)) +- Test assertions on declassifying OIDC tokens + ([#3773](https://github.com/ory/kratos/issues/3773)) + ([7f8a7f1](https://github.com/ory/kratos/commit/7f8a7f142a91c8c74f32eadb41224fc4f69c2109)) +- Tolerate more "truthy" values when creating new flows + ([#3841](https://github.com/ory/kratos/issues/3841)) + ([49d93c0](https://github.com/ory/kratos/commit/49d93c0e3383f602fe6be3c7bf749b54f344aa72)), + closes [#3839](https://github.com/ory/kratos/issues/3839): + + Use strconv.ParseBool to accept multiple "truthy" values for the `refresh` and + `return_session_token_exchange_code` query parameters when creating a new + login flow. + + For some SDKs (e.g.: Python), these stringification of booleans is not + user-controlled and these endpoints could not be used fully due to the backend + ignoring any value other than `true` (all lowercase). + +- Tweaks to UpsertSessions ([#3878](https://github.com/ory/kratos/issues/3878)) + ([da51dcd](https://github.com/ory/kratos/commit/da51dcdb8c82a5dbd290ab2f48ad74a1c6dd18f0)) +- Use correct post-verification identity state in post-hooks + ([#3863](https://github.com/ory/kratos/issues/3863)) + ([6e63d06](https://github.com/ory/kratos/commit/6e63d06db1cd1ab62f8a2d0b202ec74572420204)) +- Webhook transient payload in OIDC login flows + ([#3857](https://github.com/ory/kratos/issues/3857)) + ([2cdfc70](https://github.com/ory/kratos/commit/2cdfc70c726a166790b98d419895f0396d13176f)): + + - fix: transient payload with OIDC login + +### Code Generation + +- Pin v1.2.0 release commit + ([1a70648](https://github.com/ory/kratos/commit/1a70648c4d5b9b8d135dd7bea3842057e67b574e)) + +### Documentation + +- Remove delete reference from batch patch identity + ([#3906](https://github.com/ory/kratos/issues/3906)) + ([cd01cb9](https://github.com/ory/kratos/commit/cd01cb9fb23a24e52d46538a9ea63c2144c3b145)) + +### Features + +- Add `include_credential` query param to `/admin/identities` list call + ([#3343](https://github.com/ory/kratos/issues/3343)) + ([d94530a](https://github.com/ory/kratos/commit/d94530a716358895b01b65babd77226fab69f494)) +- Add headers to web hooks ([#3849](https://github.com/ory/kratos/issues/3849)) + ([4642de0](https://github.com/ory/kratos/commit/4642de0cfd1fb15bc48c7093be9449abd488755c)) +- Add session to post login webhook + ([#3877](https://github.com/ory/kratos/issues/3877)) + ([386078e](https://github.com/ory/kratos/commit/386078e0b5c74c54ce2c7dc6fd12fd865817b87a)) +- Add transient payloads to all flows + ([#3738](https://github.com/ory/kratos/issues/3738)) + ([b8b747b](https://github.com/ory/kratos/commit/b8b747b2adc59c8cf938a0ee30accdb4135634b8)) +- Add twitter SSO ([#3778](https://github.com/ory/kratos/issues/3778)) + ([930fb19](https://github.com/ory/kratos/commit/930fb19842e527e5e9c415efa983b36e02829516)) +- Add verification hook to login flow + ([#3829](https://github.com/ory/kratos/issues/3829)) + ([43e4ead](https://github.com/ory/kratos/commit/43e4eadce7fa6e66bf1f9c03136d141bffd3094f)) +- Allow admin to create API code recovery flows + ([#3939](https://github.com/ory/kratos/issues/3939)) + ([25d1ecd](https://github.com/ory/kratos/commit/25d1ecd90317193095e01b97ff21d92920035b02)) +- Control edge cache ttl ([#3808](https://github.com/ory/kratos/issues/3808)) + ([c9dcce5](https://github.com/ory/kratos/commit/c9dcce5a41137937df1aad7ac81170b443740f88)) +- Linkedin v2 provider ([#3804](https://github.com/ory/kratos/issues/3804)) + ([a6ad983](https://github.com/ory/kratos/commit/a6ad983ac83aa3ea65c4dc0c46b582096574c25a)): + + - feat: add linkedin-v2 provider + + - docs: document linkedin special-case + +- PassKeys with Resident Keys and two-step registration + ([#3748](https://github.com/ory/kratos/issues/3748)) + ([3621411](https://github.com/ory/kratos/commit/3621411dc4386d841bc6766a5ab8d03e65812073)) +- Send OIDC claim keys to tracing + ([#3798](https://github.com/ory/kratos/issues/3798)) + ([04390be](https://github.com/ory/kratos/commit/04390bee426befe51af2ee8177afabaa9ce4fa80)) +- Use authenticate endpoint for x + ([#3833](https://github.com/ory/kratos/issues/3833)) + ([3d9ba5d](https://github.com/ory/kratos/commit/3d9ba5df85e0d0c4d8002365987e536b37678104)): + + Improves the "Log in with X" experience by not asking the user to + re-authenticate every time. + +### Tests + +- Deflake session test ([#3864](https://github.com/ory/kratos/issues/3864)) + ([6b275f3](https://github.com/ory/kratos/commit/6b275f35a0732ffb723d47df5b6afbdc06eaf71f)) +- Resolve failing test for empty tokens + ([#3775](https://github.com/ory/kratos/issues/3775)) + ([7277368](https://github.com/ory/kratos/commit/7277368bc28df8f0badffc7e739cef20f05e9a02)) +- Resolve flaky e2e tests ([#3935](https://github.com/ory/kratos/issues/3935)) + ([a14927d](https://github.com/ory/kratos/commit/a14927dfa5f8d0fbda7e5a831f0a09a42369e06c)): + + - test: resolve flaky code registration tests + + - chore: don't fail logout if cookie is not found + + - chore: remove .only + + - chore: reduce wait + + - chore: u + + - chore: u + + - chore: u + +### Unclassified + +- Remove unnecessary COPY command from Dockerfile (#3771) + ([087748c](https://github.com/ory/kratos/commit/087748c0651ff0fc93259f7ab6b10668c09f5eba)), + closes [#3771](https://github.com/ory/kratos/issues/3771) + +# [1.1.0](https://github.com/ory/kratos/compare/v1.0.0...v1.1.0) (2024-02-20) + +![Ory Kratos v1.1.0](https://www.ory.sh/images/newsletter/kratos-1.1.0/banner.png) + +Ory Kratos v1.1 is the most complete, most scalable, and most secure open-source +identity server on the planet, and we are thrilled to announce its release! This +release comes with over 270 commits and an incredible amount of new features and +capabilities! + +- **Phone Verification & 2FA with SMS**: Enhance convenient security with phone + verification and two-factor authentication (2FA) via SMS, integrating easily + with SMS gateways like Twilio. This feature not only adds a convenient layer + of security but also offers a straightforward method for user verification, + increasing your trust in user accounts. +- **Translations & Internationalization**: Ory Kratos now supports multiple + languages, making it accessible to a global audience. This improvement + enhances the user experience by providing a localized interface, ensuring + users interact with the system in their preferred language. +- **Native Support for Sign in with Google and Apple on Android/iOS**: Get more + sign-ups with native support for "Sign in with Google" and "Sign in with + Apple" on mobile platforms. Great user experience matters! +- **Account Linking**: Simplify user management with new features that + facilitate account linking. If a user registers with a password and later + signs in with a social account sharing the same email, new screens make + account linking straightforward, enhancing user convenience and reducing + support inquiries. +- **Passwordless "Magic Code"**: Introduce a passwordless login method with + "Magic Code," which sends a one-time code to the user's email for sign-up and + login. This method can also serve as a fallback when users forget their + password or their social login is unavailable, streamlining the login process + and improving user accessibility. +- **Session to JWT Conversion**: Convert an Ory Session Cookie or Ory Session + Token into a JSON Web Token (JWT), providing more flexibility in handling + sessions and integrating with other systems. This feature allows for seamless + authentication and authorization processes across different platforms and + services. + +**Note:** To ensure a seamless upgrade experience with minimal impact, some of +these features are gated behind the `feature_flags` config parameter, allowing +controlled deployment and testing. + +The following features have been shipped exclusively to Ory Network for this +version: + +- **[B2B SSO](https://www.ory.sh/docs/kratos/organizations)** allows your + customers to connect their LDAP / Okta / AD / … to your login. Ory selects the + correct login provider based on the user’s email domain. +- [\*\*Significantly better API performance](https://www.ory.sh/docs/api/eventual-consistency)\*\* + for expensive API operations by specifying the desired consistency + (`strong`, `eventual`). +- **Finding users effortlessly** with our new fuzzy search for credential + identifiers available for + the [Identity List API](https://www.ory.sh/docs/kratos/reference/api#tag/identity/operation/listIdentities). + +- Better reliability when sending out emails across different providers. +- Streamlining the HTTP API and improving related SDK methods. +- Better performance when calling the whoami API endpoint, updating identities, + and listing identities. +- The performance of listing identities has significantly improved with the + introduction of keyset pagination. Page pagination is still available but will + be fully deprecated soon. +- Ability to list multiple identities in a batch call. +- Passkeys and WebAuthn now support multiple origins, useful when working with + subdomains. +- The logout flow now redirects the user back to the `return_to` parameter set + in the API call. +- When updating their settings, the user was sometimes incorrectly asked to + confirm the changes by providing their password. This issue has now been + fixed. +- When signing up with an account that already exists, the user will be shown a + hint helping them sign in to their existing account. +- CORS configuration can now be hot-reloaded. +- The integration with Ory OAuth2 / Ory Hydra has improved for logout, login + session management, verification, and recovery flows. +- A new passwordless method has been added: "Magic code". It sends a one-time + code to the user's email during sign-up and log-in. This method can + additionally be used as a fallback login method when the user forgets their + password. +- Integration with social sign-in has improved, and it is now possible to use + the email verified status from the social sign-in provider. +- Ory Elements and the default Ory Account Experience are now internationalized + with translations. +- It is now possible to convert an Ory Session Cookie or Ory Session Token into + a JSON Web Token. +- Recovery on native apps has improved significantly and no longer requires the + user to switch to a browser for the recovery step. +- Administrators can now find users by their identifiers with fuzzy search - + this feature is still in preview. +- Importing HMAC-hashed passwords is now possible. +- Webhooks can now update identity admin metadata. +- New screens have been added to make account linking possible when a user has + registered with a password and later tries signing in with a social account + sharing the same email. +- Ability to revoke all sessions of a user when they change their password. +- Webhooks are now available for all login, registration, and login methods, + including Passkeys, TOTP, and others. +- The login screen now longer shows “ID” for the primary identifier, but instead + extracts the correct label - for example, “Email” or “Username” from the + Identity Schema. +- Login hints help users with guidance when they are unable to sign in (wrong + social sign-in provider) but have an active account. +- Phone numbers can now be verified via an SMS gateway like Twilio. +- SMS OTP is now a two-factor option. Ory Kratos 1.1 is a major release that + marks a significant milestone in our journey. + +We sincerely hope that you find these new features and improvements in Ory +Kratos 1.1 valuable for your projects. To experience the power of the latest +release, we encourage you to get the latest version of Ory +Kratos [here](https://github.com/ory/kratos) or leverage Ory Kratos +in [Ory Network](https://www.ory.sh/network/) — the easiest, simplest, and most +cost-effective way to run Ory. + +For organizations seeking to upgrade their self-hosted solution, **Ory offers +enterprise support services to ensure a smooth transition**. Our team is ready +to assist you throughout the migration process, ensuring uninterrupted access to +the latest features and improvements. Additionally, we provide +various [support plans](https://www.ory.sh/support/) specifically tailored for +self-hosting organizations. These plans offer comprehensive assistance and +guidance to optimize your Ory deployments and meet your unique requirements. We +extend our heartfelt gratitude to the vibrant and supportive Ory Community. +Without your constant support, feedback, and contributions, reaching this +significant milestone would not have been possible. As we continue on this +journey, your feedback and suggestions are invaluable to us. Together, we are +shaping the future of identity management and authentication in the digital +landscape. + +Contributors to this release in no particular +order: [moose115](https://github.com/ory/kratos/commits?author=moose115), [K3das](https://github.com/ory/kratos/commits?author=K3das), [sidartha](https://github.com/ory/kratos/commits?author=sidartha), [efesler](https://github.com/ory/kratos/commits?author=efesler), [BrandonNoad](https://github.com/ory/kratos/commits?author=BrandonNoad) ,[Saancreed](https://github.com/ory/kratos/commits?author=Saancreed), [jpogorzelski](https://github.com/ory/kratos/commits?author=jpogorzelski), [dreksx](https://github.com/ory/kratos/commits?author=dreksx), [martinloesethjensen](https://github.com/ory/kratos/commits?author=martinloesethjensen), [cpoyatos1](https://github.com/ory/kratos/commits?author=cpoyatos1), [misamu](https://github.com/ory/kratos/commits?author=misamu), [tristankenney](https://github.com/ory/kratos/commits?author=tristankenney), [nxy7](https://github.com/ory/kratos/commits?author=nxy7), [anhnmt](https://github.com/ory/kratos/commits?author=anhnmt) + +Are you passionate about security and want to make a meaningful impact in one of +the biggest open-source communities? Join +the [Ory community](https://slack.ory.sh/) and become a part of the new ID +stack. Together, we are building the next generation of IAM solutions that +empower organizations and individuals to secure their identities effectively. +Want to check out Ory Kratos yourself? Use these commands to get your Ory Kratos +project running on the Ory Network: + +``` +brew install ory/tap/cli + +scoop bucket add ory +scoop install ory + +bash <(curl ) -b . ory +sudo mv ./ory /usr/local/bin/ + +ory auth login + +ory create project --name "My first Kratos project" + +ory open account-experience registration + +ory patch identity-config \ + --replace '/identity/default_schema_id="preset://username"' \ + --replace '/identity/schemas=[{"id":"preset://username","url":"preset://username"}]' \ + --format yaml + +ory open account-experience registration +``` ## Breaking Changes @@ -413,6 +1288,8 @@ https://github.com/ory/kratos/pull/3480 - Add caching to Jsonnet snippet during session JWT tokenization ([#3699](https://github.com/ory/kratos/issues/3699)) ([1da8180](https://github.com/ory/kratos/commit/1da818072154baa5c0921134919afde595031e94)) +- Add consistency flag ([#3733](https://github.com/ory/kratos/issues/3733)) + ([fd79950](https://github.com/ory/kratos/commit/fd7995077307cc101550eda5d7724ea1f68fa98a)) - Add max-age to default cors headers ([#3584](https://github.com/ory/kratos/issues/3584)) ([c5b4aaa](https://github.com/ory/kratos/commit/c5b4aaa2df5d010b62a99ccf45850583daad3a66)) @@ -496,6 +1373,9 @@ https://github.com/ory/kratos/pull/3480 - Don't list org SSOs in settings ([#3637](https://github.com/ory/kratos/issues/3637)) ([6c7068c](https://github.com/ory/kratos/commit/6c7068cf41df51cde5fe9fc79cca84ec6124d38a)) +- Don't require code credential for MFA flows + ([#3753](https://github.com/ory/kratos/issues/3753)) + ([40ed809](https://github.com/ory/kratos/commit/40ed809db631149874864f216a106c43ea5df670)) - Don't require session for OIDC verification ([#3443](https://github.com/ory/kratos/issues/3443)) ([e08f831](https://github.com/ory/kratos/commit/e08f831c2715e515bf58dc2dbb47fc3576421a5c)) @@ -520,6 +1400,9 @@ https://github.com/ory/kratos/pull/3480 - False-positives for requiring re-authentication on update ([#3421](https://github.com/ory/kratos/issues/3421)) ([ce8139f](https://github.com/ory/kratos/commit/ce8139f2325a8317388cbcaaa98f3f83d626657b)) +- Http courier using should use lower case json + ([#3740](https://github.com/ory/kratos/issues/3740)) + ([84149c4](https://github.com/ory/kratos/commit/84149c4b420ea89f0a16a579c017a8e7e1670204)) - Identity list pagination in CLI command and SDK ([#3482](https://github.com/ory/kratos/issues/3482)) ([1e8b1ae](https://github.com/ory/kratos/commit/1e8b1aeb4bf866892788986f62a31255372de999)): @@ -709,6 +1592,9 @@ https://github.com/ory/kratos/pull/3480 - Schema test errors ([#3528](https://github.com/ory/kratos/issues/3528)) ([bee0341](https://github.com/ory/kratos/commit/bee0341c5bf5708a2210146fc59f050a1b9df663)) +- Set iss from userinfo claims if missing + ([#3744](https://github.com/ory/kratos/issues/3744)) + ([241a911](https://github.com/ory/kratos/commit/241a911af74e8ad7353d6e3cab86db20758b86fc)) - Specify correct minimum versions in migratest ([18b89ea](https://github.com/ory/kratos/commit/18b89ea588d129fa88379f7b0d7f4fd00ec6023d)) - Tracing context passing in /sessions/whoami @@ -750,8 +1636,8 @@ https://github.com/ory/kratos/pull/3480 ### Code Generation -- Pin v1.1.0-pre.0 release commit - ([1c3eeb7](https://github.com/ory/kratos/commit/1c3eeb71d6fc83918ac367d1654361f8fd98a93e)) +- Pin v1.1.0 release commit + ([f47675b](https://github.com/ory/kratos/commit/f47675b82012e0ff74b05b9b7e713b3aa2fdda54)) ### Documentation @@ -797,6 +1683,8 @@ https://github.com/ory/kratos/pull/3480 - Add OpenTelemetry span for password hash comparison ([#3383](https://github.com/ory/kratos/issues/3383)) ([e3fcf0c](https://github.com/ory/kratos/commit/e3fcf0c31db9742ed61bcf783e37ee119ed19d42)) +- Add request URL to email and SMS templates + ([bf5f8c3](https://github.com/ory/kratos/commit/bf5f8c3cfb2eb523a77239addb8249adf9f8b31d)) - Add sms verification for phone numbers ([#3649](https://github.com/ory/kratos/issues/3649)) ([e3a3c4f](https://github.com/ory/kratos/commit/e3a3c4fe0d6697f6864283daf4be8a8f8971c7b4)) @@ -989,6 +1877,8 @@ https://github.com/ory/kratos/pull/3480 - Improve performance by computing password hashes while validating ([#3508](https://github.com/ory/kratos/issues/3508)) ([a9786c5](https://github.com/ory/kratos/commit/a9786c599d09f61e2e07df5066ce94feb2d99bac)) +- Improved webhook tracing ([#3746](https://github.com/ory/kratos/issues/3746)) + ([9d7021d](https://github.com/ory/kratos/commit/9d7021d87f47690c2c1f8000e87b425e49bc9496)) - Jsonnet caching for OIDC claims mapper, webhooks, JWT session tokenizer ([#3701](https://github.com/ory/kratos/issues/3701)) ([1d26e09](https://github.com/ory/kratos/commit/1d26e097b273aeda36f73637765da5bdb2aa4a66)) @@ -1005,6 +1895,8 @@ https://github.com/ory/kratos/pull/3480 allows user to link OIDC credentials to existing account right in the login flow, without switching to settings flow. +- List by OIDC cred ([#3721](https://github.com/ory/kratos/issues/3721)) + ([bff9c61](https://github.com/ory/kratos/commit/bff9c61b147648ab139e7e86cda4336b5d1cfd39)) - Login with code on any credential type ([#3549](https://github.com/ory/kratos/issues/3549)) ([ceed7d5](https://github.com/ory/kratos/commit/ceed7d5478c5cca894587698c57f676dda100b27)): @@ -1115,6 +2007,8 @@ https://github.com/ory/kratos/pull/3480 - Fix e2e failures and speed up e2e tests ([#3483](https://github.com/ory/kratos/issues/3483)) ([70a6171](https://github.com/ory/kratos/commit/70a617194d61763f4b75691b22cfa76ba71ab019)) +- Fix hydra tests on master ([#3737](https://github.com/ory/kratos/issues/3737)) + ([12166b4](https://github.com/ory/kratos/commit/12166b4370d607a069f268227752bb7b18a50b57)) - Reduce logging in go tests ([#3562](https://github.com/ory/kratos/issues/3562)) ([05de3a2](https://github.com/ory/kratos/commit/05de3a29fed020593c44ea7a7b29e45197fef4f7)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8c427d07090b..9275c934a84f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,7 +64,7 @@ won't clash with Ory Kratos's direction. A great way to do this is via [a Contributors License Agreement?](https://cla-assistant.io/ory/kratos) - I would like updates about new versions of Ory Kratos. - [How are new releases announced?](https://ory.us10.list-manage.com/subscribe?u=ffb1a878e4ec6c0ed312a3480&id=f605a41b53) + [How are new releases announced?](https://www.ory.sh/l/sign-up-newsletter) ## How can I contribute? @@ -144,10 +144,12 @@ checklist to contribute an example: 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 Kratos](https://github.com/ory/kratos/blob/master/.github/semantic.yml) - for an overview. + [Convential Commits configuration](https://github.com/ory/kratos/blob/master/.github/workflows/conventional_commits.yml) + for the list of accepted prefixes. You can read more about the Conventional + Commit specification + [at their site](https://www.conventionalcommits.org/en/v1.0.0/). 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)). + [the README template](https://github.com/ory/examples/blob/master/_common/README.md)). 1. Open a pull request and maintainers will review and merge your example. ## Contribute code @@ -172,8 +174,10 @@ request, go through this checklist: 1. Run `make format` 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 Kratos](https://github.com/ory/kratos/blob/master/.github/semantic.yml) - for an overview. + [Convential Commits configuration](https://github.com/ory/kratos/blob/master/.github/workflows/conventional_commits.yml) + for the list of accepted prefixes. You can read more about the Conventional + Commit specification + [at their site](https://www.conventionalcommits.org/en/v1.0.0/). 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). diff --git a/Makefile b/Makefile index 1775d09b25ea..05c9c0de9760 100644 --- a/Makefile +++ b/Makefile @@ -32,9 +32,8 @@ $(call make-lint-dependency) echo "deprecated usage, use docs/cli instead" go build -o .bin/clidoc ./cmd/clidoc/. -.PHONY: .bin/yq -.bin/yq: - go build -o .bin/yq github.com/mikefarah/yq/v4 +.bin/yq: Makefile + GOBIN=$(PWD)/.bin go install github.com/mikefarah/yq/v4@v4.44.3 .PHONY: docs/cli docs/cli: @@ -49,7 +48,7 @@ docs/swagger: npx @redocly/openapi-cli preview-docs spec/swagger.json .bin/golangci-lint: Makefile - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -d -b .bin v1.55.2 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -d -b .bin v1.61.0 .bin/hydra: Makefile bash <(curl https://raw.githubusercontent.com/ory/meta/master/install.sh) -d -b .bin hydra v2.2.0-rc.3 @@ -58,17 +57,31 @@ docs/swagger: curl https://raw.githubusercontent.com/ory/meta/master/install.sh | bash -s -- -b .bin ory v0.2.2 touch -a -m .bin/ory +.bin/buf: Makefile + curl -sSL \ + "https://github.com/bufbuild/buf/releases/download/v1.39.0/buf-$(shell uname -s)-$(shell uname -m).tar.gz" | \ + tar -xvzf - -C ".bin/" --strip-components=2 buf/bin/buf buf/bin/protoc-gen-buf-breaking buf/bin/protoc-gen-buf-lint + touch -a -m .bin/buf + .PHONY: lint lint: .bin/golangci-lint - golangci-lint run -v --timeout 10m ./... + .bin/golangci-lint run -v --timeout 10m ./... + .bin/buf lint .PHONY: mocks mocks: .bin/mockgen mockgen -mock_names Manager=MockLoginExecutorDependencies -package internal -destination internal/hook_login_executor_dependencies.go github.com/ory/kratos/selfservice loginExecutorDependencies +.PHONY: proto +proto: gen/oidc/v1/state.pb.go + +gen/oidc/v1/state.pb.go: proto/oidc/v1/state.proto buf.yaml buf.gen.yaml .bin/buf .bin/goimports + .bin/buf generate + .bin/goimports -w gen/ + .PHONY: install install: - GO111MODULE=on go install -tags sqlite . + go install -tags sqlite . .PHONY: test-resetdb test-resetdb: @@ -125,7 +138,7 @@ sdk: .bin/swagger .bin/ory node_modules --git-user-id ory \ --git-repo-id client-go \ --git-host github.com \ - --api-name-suffix "Api" \ + --api-name-suffix "API" \ -t .schema/openapi/templates/go \ -c .schema/openapi/gen.go.yml @@ -139,7 +152,7 @@ sdk: .bin/swagger .bin/ory node_modules --git-user-id ory \ --git-repo-id client-go \ --git-host github.com \ - --api-name-suffix "Api" \ + --api-name-suffix "API" \ -t .schema/openapi/templates/go \ -c .schema/openapi/gen.go.yml @@ -163,11 +176,12 @@ authors: # updates the AUTHORS file # Formats the code .PHONY: format -format: .bin/goimports .bin/ory node_modules - .bin/ory dev headers copyright --exclude=internal/httpclient --exclude=internal/client-go --exclude test/e2e/proxy/node_modules --exclude test/e2e/node_modules --exclude node_modules +format: .bin/goimports .bin/ory node_modules .bin/buf + .bin/ory dev headers copyright --exclude=gen --exclude=internal/httpclient --exclude=internal/client-go --exclude test/e2e/proxy/node_modules --exclude test/e2e/node_modules --exclude node_modules goimports -w -local github.com/ory . npm exec -- prettier --write 'test/e2e/**/*{.ts,.js}' npm exec -- prettier --write '.github' + .bin/buf format --write # Build local docker image .PHONY: docker @@ -193,8 +207,8 @@ migrations-sync: .bin/ory ory dev pop migration sync persistence/sql/migrations/templates persistence/sql/migratest/testdata script/add-down-migrations.sh -.PHONY: test-update-snapshots -test-update-snapshots: +.PHONY: test-refresh +test-refresh: UPDATE_SNAPSHOTS=true go test -tags sqlite,json1,refresh -short ./... .PHONY: post-release diff --git a/README.md b/README.md index c74bb402836c..decd913076e0 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,11 @@

Chat | Discussions | - Newsletter

+ Newsletter

Guide | API Docs | Code Docs

- Support this project!

+ Support this project!

Work in Open Source, Ory is hiring!

@@ -554,7 +554,7 @@ that your company deserves a spot here, reach out to pinniped.dev - + Adopter * Pvotal diff --git a/SECURITY.md b/SECURITY.md index 7a05c1cfc62e..026e3afb70f8 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,30 +1,53 @@ - - - -- [Security Policy](#security-policy) - - [Supported Versions](#supported-versions) - - [Reporting a Vulnerability](#reporting-a-vulnerability) - - - -# Security Policy - -## Supported Versions - -We release patches for security vulnerabilities. Which versions are eligible for -receiving such patches depends on the CVSS v3.0 Rating: - -| CVSS v3.0 | Supported Versions | -| --------- | ----------------------------------------- | -| 9.0-10.0 | Releases within the previous three months | -| 4.0-8.9 | Most recent release | +# Ory Security Policy + +## Overview + +This security policy outlines the security support commitments for different +types of Ory users. + +[Get in touch](https://www.ory.sh/contact/) to learn more about Ory's security +SLAs and process. + +## Apache 2.0 License Users + +- **Security SLA:** No security Service Level Agreement (SLA) is provided. +- **Release Schedule:** Releases are planned every 3 to 6 months. These releases + will contain all security fixes implemented up to that point. +- **Version Support:** Security patches are only provided for the current + release version. + +## Ory Enterprise License Customers + +- **Security SLA:** The following timelines apply for security vulnerabilities + based on their severity: + - Critical: Resolved within 14 days. + - High: Resolved within 30 days. + - Medium: Resolved within 90 days. + - Low: Resolved within 180 days. + - Informational: Addressed as needed. +- **Release Schedule:** Updates are provided as soon as vulnerabilities are + resolved, adhering to the above SLA. +- **Version Support:** Depending on the Ory Enterprise License agreement + multiple versions can be supported. + +## Ory Network Users + +- **Security SLA:** The following timelines apply for security vulnerabilities + based on their severity: + - Critical: Resolved within 14 days. + - High: Resolved within 30 days. + - Medium: Resolved within 90 days. + - Low: Resolved within 180 days. + - Informational: Addressed as needed. +- **Release Schedule:** Updates are automatically deployed to Ory Network as + soon as vulnerabilities are resolved, adhering to the above SLA. +- **Version Support:** Ory Network always runs the most current version. ## 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 head over to our +[security policy](https://www.ory.sh/docs/ecosystem/security) to learn more +about reporting security vulnerabilities. diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 000000000000..bcc94c85856e --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,12 @@ +version: v2 +managed: + enabled: true + override: + - file_option: go_package_prefix + value: github.com/ory/kratos +plugins: + - remote: buf.build/protocolbuffers/go + out: gen + opt: paths=source_relative +inputs: + - directory: proto diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 000000000000..227c4a6c6faf --- /dev/null +++ b/buf.yaml @@ -0,0 +1,9 @@ +version: v2 +modules: + - path: proto +lint: + use: + - DEFAULT +breaking: + use: + - FILE diff --git a/cipher/aes.go b/cipher/aes.go index f4bae7e98001..7c34651d5e3f 100644 --- a/cipher/aes.go +++ b/cipher/aes.go @@ -12,19 +12,13 @@ import ( "github.com/ory/herodot" "github.com/pkg/errors" - - "github.com/ory/kratos/driver/config" ) type AES struct { - c AESConfiguration -} - -type AESConfiguration interface { - config.Provider + c SecretsProvider } -func NewCryptAES(c AESConfiguration) *AES { +func NewCryptAES(c SecretsProvider) *AES { return &AES{c: c} } @@ -36,11 +30,11 @@ func (a *AES) Encrypt(ctx context.Context, message []byte) (string, error) { return "", nil } - if len(a.c.Config().SecretsCipher(ctx)) == 0 { + if len(a.c.SecretsCipher(ctx)) == 0 { return "", errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to encrypt message because no cipher secrets were configured.")) } - ciphertext, err := cryptopasta.Encrypt(message, &a.c.Config().SecretsCipher(ctx)[0]) + ciphertext, err := cryptopasta.Encrypt(message, &a.c.SecretsCipher(ctx)[0]) return hex.EncodeToString(ciphertext), errors.WithStack(err) } @@ -52,7 +46,7 @@ func (a *AES) Decrypt(ctx context.Context, ciphertext string) ([]byte, error) { return nil, nil } - secrets := a.c.Config().SecretsCipher(ctx) + secrets := a.c.SecretsCipher(ctx) if len(secrets) == 0 { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to decipher the encrypted message because no AES secrets were configured.")) } diff --git a/cipher/chacha20.go b/cipher/chacha20.go index 6ee73ea055f8..6ad71746c895 100644 --- a/cipher/chacha20.go +++ b/cipher/chacha20.go @@ -8,23 +8,19 @@ import ( "crypto/rand" "encoding/hex" "io" + "math" "github.com/pkg/errors" "golang.org/x/crypto/chacha20poly1305" "github.com/ory/herodot" - "github.com/ory/kratos/driver/config" ) -type ChaCha20Configuration interface { - config.Provider -} - type XChaCha20Poly1305 struct { - c ChaCha20Configuration + c SecretsProvider } -func NewCryptChaCha20(c ChaCha20Configuration) *XChaCha20Poly1305 { +func NewCryptChaCha20(c SecretsProvider) *XChaCha20Poly1305 { return &XChaCha20Poly1305{c: c} } @@ -34,15 +30,20 @@ func (c *XChaCha20Poly1305) Encrypt(ctx context.Context, message []byte) (string return "", nil } - if len(c.c.Config().SecretsCipher(ctx)) == 0 { + if len(c.c.SecretsCipher(ctx)) == 0 { return "", errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to encrypt message because no cipher secrets were configured.")) } - aead, err := chacha20poly1305.NewX(c.c.Config().SecretsCipher(ctx)[0][:]) + aead, err := chacha20poly1305.NewX(c.c.SecretsCipher(ctx)[0][:]) if err != nil { return "", herodot.ErrInternalServerError.WithWrap(err).WithReason("Unable to generate key") } + // Make sure the size calculation does not overflow. + if len(message) > math.MaxInt-aead.NonceSize()-aead.Overhead() { + return "", errors.WithStack(herodot.ErrInternalServerError.WithReason("plaintext too large")) + } + nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(message)+aead.Overhead()) _, err = io.ReadFull(rand.Reader, nonce) if err != nil { @@ -59,7 +60,7 @@ func (c *XChaCha20Poly1305) Decrypt(ctx context.Context, ciphertext string) ([]b return nil, nil } - secrets := c.c.Config().SecretsCipher(ctx) + secrets := c.c.SecretsCipher(ctx) if len(secrets) == 0 { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to decipher the encrypted message because no cipher secrets were configured.")) } @@ -72,7 +73,7 @@ func (c *XChaCha20Poly1305) Decrypt(ctx context.Context, ciphertext string) ([]b for i := range secrets { aead, err := chacha20poly1305.NewX(secrets[i][:]) if err != nil { - return nil, errors.WithStack(herodot.ErrInternalServerError.WithWrap(err).WithReason("Unable to instanciate chacha20")) + return nil, errors.WithStack(herodot.ErrInternalServerError.WithWrap(err).WithReason("Unable to instantiate chacha20")) } if len(ciphertext) < aead.NonceSize() { diff --git a/cipher/cipher.go b/cipher/cipher.go index 27fc316c2cfa..73c2a0b3b5c3 100644 --- a/cipher/cipher.go +++ b/cipher/cipher.go @@ -23,3 +23,7 @@ type Cipher interface { type Provider interface { Cipher(ctx context.Context) Cipher } + +type SecretsProvider interface { + SecretsCipher(ctx context.Context) [][32]byte +} diff --git a/cipher/cipher_test.go b/cipher/cipher_test.go index 8cdb0ed0e2ac..1680622517d8 100644 --- a/cipher/cipher_test.go +++ b/cipher/cipher_test.go @@ -9,6 +9,10 @@ import ( "fmt" "testing" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + + "github.com/ory/x/configx" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -18,94 +22,84 @@ import ( "github.com/ory/kratos/internal" ) +var goodSecret = []string{"secret-thirty-two-character-long"} + func TestCipher(t *testing.T) { ctx := context.Background() - cfg, reg := internal.NewFastRegistryWithMocks(t) - goodSecret := []string{"secret-thirty-two-character-long"} + _, reg := internal.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeySecretsDefault, goodSecret)) ciphers := []cipher.Cipher{ - cipher.NewCryptAES(reg), - cipher.NewCryptChaCha20(reg), + cipher.NewCryptAES(reg.Config()), + cipher.NewCryptChaCha20(reg.Config()), } for _, c := range ciphers { t.Run(fmt.Sprintf("cipher=%T", c), func(t *testing.T) { + t.Parallel() t.Run("case=all_work", func(t *testing.T) { - cfg.MustSet(ctx, config.ViperKeySecretsCipher, goodSecret) - testAllWork(t, c, cfg) + t.Parallel() + + testAllWork(ctx, t, c) }) t.Run("case=encryption_failed", func(t *testing.T) { - // unset secret - err := cfg.Set(ctx, config.ViperKeySecretsCipher, []string{}) - require.NoError(t, err) + t.Parallel() + + ctx := confighelpers.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""}) // secret have to be set - _, err = c.Encrypt(context.Background(), []byte("not-empty")) + _, err := c.Encrypt(ctx, []byte("not-empty")) require.Error(t, err) + var hErr *herodot.DefaultError + require.ErrorAs(t, err, &hErr) + assert.Equal(t, "Unable to encrypt message because no cipher secrets were configured.", hErr.Reason()) - // unset secret - err = cfg.Set(ctx, config.ViperKeySecretsCipher, []string{"bad-length"}) - require.NoError(t, err) + ctx = confighelpers.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{"bad-length"}) // bad secret length - _, err = c.Encrypt(context.Background(), []byte("not-empty")) - if e, ok := err.(*herodot.DefaultError); ok { - t.Logf("reason contains: %s", e.Reason()) - } - t.Logf("err type %T contains: %s", err, err.Error()) - require.Error(t, err) + _, err = c.Encrypt(ctx, []byte("not-empty")) + require.ErrorAs(t, err, &hErr) + assert.Equal(t, "Unable to encrypt message because no cipher secrets were configured.", hErr.Reason()) }) t.Run("case=decryption_failed", func(t *testing.T) { - // set secret - err := cfg.Set(ctx, config.ViperKeySecretsCipher, goodSecret) - require.NoError(t, err) + t.Parallel() - // - _, err = c.Decrypt(context.Background(), hex.EncodeToString([]byte("bad-data"))) + _, err := c.Decrypt(ctx, hex.EncodeToString([]byte("bad-data"))) require.Error(t, err) - _, err = c.Decrypt(context.Background(), "not-empty") + _, err = c.Decrypt(ctx, "not-empty") require.Error(t, err) - // unset secret - err = cfg.Set(ctx, config.ViperKeySecretsCipher, []string{}) - require.NoError(t, err) - - _, err = c.Decrypt(context.Background(), "not-empty") + _, err = c.Decrypt(confighelpers.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""}), "not-empty") require.Error(t, err) }) }) } - c := cipher.NewNoop(reg) + + c := cipher.NewNoop() t.Run(fmt.Sprintf("cipher=%T", c), func(t *testing.T) { - cfg.MustSet(ctx, config.ViperKeySecretsCipher, goodSecret) - testAllWork(t, c, cfg) + t.Parallel() + testAllWork(ctx, t, c) }) } -func testAllWork(t *testing.T, c cipher.Cipher, cfg *config.Config) { - ctx := context.Background() - - goodSecret := []string{"secret-thirty-two-character-long"} - cfg.MustSet(ctx, config.ViperKeySecretsCipher, goodSecret) - +func testAllWork(ctx context.Context, t *testing.T, c cipher.Cipher) { message := "my secret message!" - encryptedSecret, err := c.Encrypt(context.Background(), []byte(message)) + encryptedSecret, err := c.Encrypt(ctx, []byte(message)) require.NoError(t, err) - decryptedSecret, err := c.Decrypt(context.Background(), encryptedSecret) + decryptedSecret, err := c.Decrypt(ctx, encryptedSecret) require.NoError(t, err, "encrypted", encryptedSecret) assert.Equal(t, message, string(decryptedSecret)) // data to encrypt return blank result - _, err = c.Encrypt(context.Background(), []byte("")) + _, err = c.Encrypt(ctx, []byte("")) require.NoError(t, err) // empty encrypted data return blank - _, err = c.Decrypt(context.Background(), "") + _, err = c.Decrypt(ctx, "") require.NoError(t, err) } diff --git a/cipher/noop.go b/cipher/noop.go index 2b2a353beb23..481d4c8bcba2 100644 --- a/cipher/noop.go +++ b/cipher/noop.go @@ -6,30 +6,21 @@ package cipher import ( "context" "encoding/hex" - - "github.com/ory/kratos/driver/config" ) // Noop is default cipher implementation witch does not do encryption +type Noop struct{} -type NoopConfiguration interface { - config.Provider -} - -type Noop struct { - c NoopConfiguration -} - -func NewNoop(c NoopConfiguration) *Noop { - return &Noop{c: c} +func NewNoop() *Noop { + return &Noop{} } // Encrypt encode message to hex -func (c *Noop) Encrypt(_ context.Context, message []byte) (string, error) { +func (*Noop) Encrypt(_ context.Context, message []byte) (string, error) { return hex.EncodeToString(message), nil } // Decrypt decode the hex message -func (c *Noop) Decrypt(_ context.Context, ciphertext string) ([]byte, error) { +func (*Noop) Decrypt(_ context.Context, ciphertext string) ([]byte, error) { return hex.DecodeString(ciphertext) } diff --git a/cmd/clidoc/main.go b/cmd/clidoc/main.go index ef3027a936a3..81b68b84b778 100644 --- a/cmd/clidoc/main.go +++ b/cmd/clidoc/main.go @@ -72,6 +72,7 @@ func init() { "NewInfoSelfServiceSettingsUpdateUnlinkOIDC": text.NewInfoSelfServiceSettingsUpdateUnlinkOIDC("{provider}"), "NewInfoSelfServiceRegisterWebAuthnDisplayName": text.NewInfoSelfServiceRegisterWebAuthnDisplayName(), "NewInfoSelfServiceRemoveWebAuthn": text.NewInfoSelfServiceRemoveWebAuthn("{display_name}", aSecondAgo), + "NewInfoSelfServiceRemovePasskey": text.NewInfoSelfServiceRemovePasskey("{display_name}", aSecondAgo), "NewErrorValidationVerificationFlowExpired": text.NewErrorValidationVerificationFlowExpired(aSecondAgo), "NewInfoSelfServiceVerificationSuccessful": text.NewInfoSelfServiceVerificationSuccessful(), "NewVerificationEmailSent": text.NewVerificationEmailSent(), @@ -120,11 +121,11 @@ func init() { "NewInfoLoginLookupLabel": text.NewInfoLoginLookupLabel(), "NewInfoLogin": text.NewInfoLogin(), "NewInfoLoginAndLink": text.NewInfoLoginAndLink(), - "NewInfoLoginLinkMessage": text.NewInfoLoginLinkMessage("{duplicteIdentifier}", "{provider}", "{newLoginUrl}"), + "NewInfoLoginLinkMessage": text.NewInfoLoginLinkMessage("{duplicateIdentifier}", "{provider}", "{newLoginUrl}", []string{"{available_credential_types_list}"}, []string{"{available_oidc_providers_list}"}), "NewInfoLoginTOTP": text.NewInfoLoginTOTP(), "NewInfoLoginLookup": text.NewInfoLoginLookup(), "NewInfoLoginVerify": text.NewInfoLoginVerify(), - "NewInfoLoginWith": text.NewInfoLoginWith("{provider}"), + "NewInfoLoginWith": text.NewInfoLoginWith("{provider}", "{providerID}"), "NewInfoLoginWithAndLink": text.NewInfoLoginWithAndLink("{provider}"), "NewErrorValidationLoginFlowExpired": text.NewErrorValidationLoginFlowExpired(aSecondAgo), "NewErrorValidationLoginNoStrategyFound": text.NewErrorValidationLoginNoStrategyFound(), @@ -134,8 +135,10 @@ func init() { "NewErrorValidationVerificationNoStrategyFound": text.NewErrorValidationVerificationNoStrategyFound(), "NewInfoSelfServiceLoginWebAuthn": text.NewInfoSelfServiceLoginWebAuthn(), "NewInfoRegistration": text.NewInfoRegistration(), - "NewInfoRegistrationWith": text.NewInfoRegistrationWith("{provider}"), + "NewInfoRegistrationWith": text.NewInfoRegistrationWith("{provider}", "{providerID}"), "NewInfoRegistrationContinue": text.NewInfoRegistrationContinue(), + "NewInfoRegistrationBack": text.NewInfoRegistrationBack(), + "NewInfoSelfServiceChooseCredentials": text.NewInfoSelfServiceChooseCredentials(), "NewErrorValidationRegistrationFlowExpired": text.NewErrorValidationRegistrationFlowExpired(aSecondAgo), "NewErrorValidationRecoveryFlowExpired": text.NewErrorValidationRecoveryFlowExpired(aSecondAgo), "NewRecoverySuccessful": text.NewRecoverySuccessful(inAMinute), @@ -150,9 +153,12 @@ func init() { "NewInfoNodeLoginAndLinkCredential": text.NewInfoNodeLoginAndLinkCredential(), "NewInfoNodeLabelContinue": text.NewInfoNodeLabelContinue(), "NewInfoSelfServiceSettingsRegisterWebAuthn": text.NewInfoSelfServiceSettingsRegisterWebAuthn(), + "NewInfoSelfServiceSettingsRegisterPasskey": text.NewInfoSelfServiceSettingsRegisterPasskey(), "NewInfoLoginWebAuthnPasswordless": text.NewInfoLoginWebAuthnPasswordless(), "NewInfoSelfServiceRegistrationRegisterWebAuthn": text.NewInfoSelfServiceRegistrationRegisterWebAuthn(), "NewInfoSelfServiceContinueLoginWebAuthn": text.NewInfoSelfServiceContinueLoginWebAuthn(), + "NewInfoSelfServiceLoginPasskey": text.NewInfoSelfServiceLoginPasskey(), + "NewInfoSelfServiceRegistrationRegisterPasskey": text.NewInfoSelfServiceRegistrationRegisterPasskey(), "NewInfoSelfServiceLoginContinue": text.NewInfoSelfServiceLoginContinue(), "NewErrorValidationSuchNoWebAuthnUser": text.NewErrorValidationSuchNoWebAuthnUser(), "NewRegistrationEmailWithCodeSent": text.NewRegistrationEmailWithCodeSent(), @@ -170,7 +176,11 @@ func init() { "NewErrorValidationLoginLinkedCredentialsDoNotMatch": text.NewErrorValidationLoginLinkedCredentialsDoNotMatch(), "NewErrorValidationAddressUnknown": text.NewErrorValidationAddressUnknown(), "NewInfoSelfServiceLoginCodeMFA": text.NewInfoSelfServiceLoginCodeMFA(), - "NewInfoSelfServiceLoginCodeMFAHint": text.NewInfoSelfServiceLoginCodeMFAHint("{maskedIdentifier}"), + "NewInfoLoginPassword": text.NewInfoLoginPassword(), + "NewErrorValidationAccountNotFound": text.NewErrorValidationAccountNotFound(), + "NewInfoSelfServiceLoginAAL2CodeAddress": text.NewInfoSelfServiceLoginAAL2CodeAddress("{channel}", "{address}"), + "NewErrorCaptchaFailed": text.NewErrorCaptchaFailed(), + "NewCaptchaContainerMessage": text.NewCaptchaContainerMessage(), } } @@ -193,7 +203,7 @@ func main() { } } - if err := writeMessages(filepath.Join(os.Args[2], "concepts/ui-user-interface.mdx"), sortedMessages); err != nil { + if err := writeMessages(filepath.Join(os.Args[2], "concepts/ui-messages.md"), sortedMessages); err != nil { _, _ = fmt.Fprintf(os.Stderr, "Unable to generate message table: %+v\n", err) os.Exit(1) } @@ -299,6 +309,8 @@ func validateAllMessages(path string) error { info := &types.Info{ Defs: make(map[*ast.Ident]types.Object), } + + //nolint:staticcheck var pack *ast.Package for _, p := range packs { if p.Name == "text" { diff --git a/cmd/courier/watch_test.go b/cmd/courier/watch_test.go index 48fd6515f53b..b521e9119a97 100644 --- a/cmd/courier/watch_test.go +++ b/cmd/courier/watch_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/require" "github.com/ory/kratos/internal" + "github.com/ory/x/configx" ) func TestStartCourier(t *testing.T) { @@ -27,10 +28,9 @@ func TestStartCourier(t *testing.T) { t.Run("case=with metrics", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) - _, r := internal.NewFastRegistryWithMocks(t) port, err := freeport.GetFreePort() require.NoError(t, err) - r.Config().Set(ctx, "expose-metrics-port", port) + _, r := internal.NewFastRegistryWithMocks(t, configx.WithValue("expose-metrics-port", port)) go StartCourier(ctx, r) time.Sleep(time.Second) res, err := http.Get("http://" + r.Config().MetricsListenOn(ctx) + "/metrics/prometheus") diff --git a/cmd/daemon/serve.go b/cmd/daemon/serve.go index e68523243dd5..fb7557033e01 100644 --- a/cmd/daemon/serve.go +++ b/cmd/daemon/serve.go @@ -116,7 +116,7 @@ func servePublic(r driver.Registry, cmd *cobra.Command, eg *errgroup.Group, slOp n.UseFunc(x.CleanPath) // Prevent double slashes from breaking CSRF. r.WithCSRFHandler(csrf) - n.UseHandler(r.CSRFHandler()) + n.UseHandler(http.MaxBytesHandler(r.CSRFHandler(), 5*1024*1024 /* 5 MB */)) // Disable CSRF for these endpoints csrf.DisablePath(healthx.AliveCheckPath) @@ -199,7 +199,7 @@ func serveAdmin(r driver.Registry, cmd *cobra.Command, eg *errgroup.Group, slOpt r.RegisterAdminRoutes(ctx, router) r.PrometheusManager().RegisterRouter(router.Router) - n.UseHandler(router) + n.UseHandler(http.MaxBytesHandler(router, 5*1024*1024 /* 5 MB */)) certs := c.GetTLSCertificatesForAdmin(ctx) var handler http.Handler = n diff --git a/cmd/hashers/argon2/root.go b/cmd/hashers/argon2/root.go index c5cb76581590..2282f0404d4a 100644 --- a/cmd/hashers/argon2/root.go +++ b/cmd/hashers/argon2/root.go @@ -9,6 +9,8 @@ import ( "reflect" "strings" + "github.com/ory/x/contextx" + "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -70,6 +72,7 @@ func configProvider(cmd *cobra.Command, flagConf *argon2Config) (*argon2Config, cmd.Context(), l, cmd.ErrOrStderr(), + &contextx.Default{}, configx.WithFlags(cmd.Flags()), configx.SkipValidation(), configx.WithContext(cmd.Context()), diff --git a/cmd/identities/delete.go b/cmd/identities/delete.go index 9a1029b7a244..e1433bca87be 100644 --- a/cmd/identities/delete.go +++ b/cmd/identities/delete.go @@ -47,7 +47,7 @@ func NewDeleteIdentityCmd() *cobra.Command { ) for _, a := range args { - _, err := c.IdentityApi.DeleteIdentity(cmd.Context(), a).Execute() + _, err := c.IdentityAPI.DeleteIdentity(cmd.Context(), a).Execute() if err != nil { failed[a] = cmdx.PrintOpenAPIError(cmd, err) continue diff --git a/cmd/identities/get.go b/cmd/identities/get.go index 677ac3bc9121..8203454b2af0 100644 --- a/cmd/identities/get.go +++ b/cmd/identities/get.go @@ -66,7 +66,7 @@ func NewGetIdentityCmd() *cobra.Command { identities := make([]kratos.Identity, 0, len(args)) failed := make(map[string]error) for _, id := range args { - identity, _, err := c.IdentityApi. + identity, _, err := c.IdentityAPI. GetIdentity(cmd.Context(), id). IncludeCredential(includeCreds). Execute() diff --git a/cmd/identities/get_test.go b/cmd/identities/get_test.go index 03a1291d5872..5cbaad0e9cb8 100644 --- a/cmd/identities/get_test.go +++ b/cmd/identities/get_test.go @@ -5,7 +5,6 @@ package identities_test import ( "context" - "encoding/hex" "encoding/json" "testing" @@ -63,10 +62,12 @@ func TestGetCmd(t *testing.T) { return out } transform := func(token string) string { - if !encrypt { - return token + if encrypt { + s, err := reg.Cipher(context.Background()).Encrypt(context.Background(), []byte(token)) + require.NoError(t, err) + return s } - return hex.EncodeToString([]byte(token)) + return token } return identity.Credentials{ Type: identity.CredentialsTypeOIDC, diff --git a/cmd/identities/helpers_test.go b/cmd/identities/helpers_test.go index 5997b32c7623..a6571e813abc 100644 --- a/cmd/identities/helpers_test.go +++ b/cmd/identities/helpers_test.go @@ -21,7 +21,7 @@ import ( "github.com/ory/kratos/internal/testhelpers" ) -func setup(t *testing.T, newCmd func() *cobra.Command) (driver.Registry, *cmdx.CommandExecuter) { +func setup(t *testing.T, newCmd func() *cobra.Command) (*driver.RegistryDefault, *cmdx.CommandExecuter) { conf, reg := internal.NewFastRegistryWithMocks(t) _, admin := testhelpers.NewKratosServerWithCSRF(t, reg) testhelpers.SetDefaultIdentitySchema(conf, "file://./stubs/identity.schema.json") diff --git a/cmd/identities/import.go b/cmd/identities/import.go index 1de8a22de385..8eac43afed05 100644 --- a/cmd/identities/import.go +++ b/cmd/identities/import.go @@ -73,7 +73,7 @@ Files can contain only a single or an array of identities. The validity of files return cmdx.FailSilently(cmd) } - ident, _, err := c.IdentityApi.CreateIdentity(cmd.Context()).CreateIdentityBody(params).Execute() + ident, _, err := c.IdentityAPI.CreateIdentity(cmd.Context()).CreateIdentityBody(params).Execute() if err != nil { failed[src] = cmdx.PrintOpenAPIError(cmd, err) } else { diff --git a/cmd/identities/list.go b/cmd/identities/list.go index 0fd4f13d20bf..24e38fafad0b 100644 --- a/cmd/identities/list.go +++ b/cmd/identities/list.go @@ -43,7 +43,7 @@ Eventual consistency means that the list operation will return faster and might } consistency := flagx.MustGetString(cmd, "consistency") - req := c.IdentityApi.ListIdentities(cmd.Context()).Consistency(consistency) + req := c.IdentityAPI.ListIdentities(cmd.Context()).Consistency(consistency) page, perPage, err := cmdx.ParseTokenPaginationArgs(cmd) if err != nil { return err diff --git a/cmd/identities/validate.go b/cmd/identities/validate.go index 2454d896225b..322e8e15c99a 100644 --- a/cmd/identities/validate.go +++ b/cmd/identities/validate.go @@ -56,7 +56,7 @@ Identities can be supplied via STD_IN or JSON files containing a single or an ar for src, i := range is { err = ValidateIdentity(cmd, src, i, func(ctx context.Context, id string) (map[string]interface{}, *http.Response, error) { - return c.IdentityApi.GetIdentitySchema(ctx, id).Execute() + return c.IdentityAPI.GetIdentitySchema(ctx, id).Execute() }) if err != nil { return err diff --git a/cmd/remote/status.go b/cmd/remote/status.go index e61ef0b068b2..7421eef7e659 100644 --- a/cmd/remote/status.go +++ b/cmd/remote/status.go @@ -51,14 +51,14 @@ var statusCmd = &cobra.Command{ state := &statusState{} defer cmdx.PrintRow(cmd, state) - alive, _, err := c.MetadataApi.IsAlive(cmd.Context()).Execute() + alive, _, err := c.MetadataAPI.IsAlive(cmd.Context()).Execute() if err != nil { return err } state.Alive = alive.Status == "ok" - ready, _, err := c.MetadataApi.IsReady(cmd.Context()).Execute() + ready, _, err := c.MetadataAPI.IsReady(cmd.Context()).Execute() if err != nil { return err } diff --git a/cmd/remote/version.go b/cmd/remote/version.go index 18c33388ac36..23bf102f344f 100644 --- a/cmd/remote/version.go +++ b/cmd/remote/version.go @@ -36,7 +36,7 @@ var versionCmd = &cobra.Command{ return err } - resp, _, err := c.MetadataApi.GetVersion(cmd.Context()).Execute() + resp, _, err := c.MetadataAPI.GetVersion(cmd.Context()).Execute() if err != nil { return err } diff --git a/continuity/container.go b/continuity/container.go index 9b2d434b859a..823942414555 100644 --- a/continuity/container.go +++ b/continuity/container.go @@ -63,7 +63,7 @@ func (c *Container) Valid(identity uuid.UUID) error { } if identity != uuid.Nil && pointerx.Deref(c.IdentityID) != identity { - return errors.WithStack(herodot.ErrBadRequest.WithReasonf("You must restart the flow because the resumable session was initiated by another person.")) + return errors.WithStack(herodot.ErrForbidden.WithReasonf("The flow has been blocked for security reasons because it was initiated by another person..")) } return nil diff --git a/continuity/manager.go b/continuity/manager.go index 65989704faa0..779b0ae5f185 100644 --- a/continuity/manager.go +++ b/continuity/manager.go @@ -27,19 +27,18 @@ type Manager interface { } type managerOptions struct { - iid uuid.UUID - ttl time.Duration - payload json.RawMessage - payloadRaw interface{} - cleanUp bool + iid uuid.UUID + ttl time.Duration + setExpiresIn time.Duration + payload json.RawMessage + payloadRaw interface{} } type ManagerOption func(*managerOptions) error func newManagerOptions(opts []ManagerOption) (*managerOptions, error) { var o = &managerOptions{ - ttl: time.Minute, - cleanUp: true, + ttl: time.Minute * 10, } for _, opt := range opts { if err := opt(o); err != nil { @@ -49,13 +48,6 @@ func newManagerOptions(opts []ManagerOption) (*managerOptions, error) { return o, nil } -func DontCleanUp() ManagerOption { - return func(o *managerOptions) error { - o.cleanUp = false - return nil - } -} - func WithIdentity(i *identity.Identity) ManagerOption { return func(o *managerOptions) error { if i != nil { @@ -83,3 +75,10 @@ func WithPayload(payload interface{}) ManagerOption { return nil } } + +func WithExpireInsteadOfDelete(duration time.Duration) ManagerOption { + return func(o *managerOptions) error { + o.setExpiresIn = duration + return nil + } +} diff --git a/continuity/manager_cookie.go b/continuity/manager_cookie.go index 495800a87736..7d9b40632df5 100644 --- a/continuity/manager_cookie.go +++ b/continuity/manager_cookie.go @@ -8,6 +8,7 @@ import ( "context" "encoding/json" "net/http" + "time" "github.com/gofrs/uuid" "github.com/pkg/errors" @@ -93,12 +94,22 @@ func (m *ManagerCookie) Continue(ctx context.Context, w http.ResponseWriter, r * } } - if err := x.SessionUnsetKey(w, r, m.d.ContinuityCookieManager(ctx), CookieName, name); err != nil { - return nil, err - } + if o.setExpiresIn > 0 { + if err := m.d.ContinuityPersister().SetContinuitySessionExpiry( + ctx, + container.ID, + time.Now().UTC().Add(o.setExpiresIn).Truncate(time.Second), + ); err != nil && !errors.Is(err, sqlcon.ErrNoRows) { + return nil, err + } + } else { + if err := x.SessionUnsetKey(w, r, m.d.ContinuityCookieManager(ctx), CookieName, name); err != nil { + return nil, err + } - if err := m.d.ContinuityPersister().DeleteContinuitySession(ctx, container.ID); err != nil && !errors.Is(err, sqlcon.ErrNoRows) { - return nil, err + if err := m.d.ContinuityPersister().DeleteContinuitySession(ctx, container.ID); err != nil && !errors.Is(err, sqlcon.ErrNoRows) { + return nil, err + } } return container, nil @@ -136,6 +147,9 @@ func (m *ManagerCookie) container(ctx context.Context, w http.ResponseWriter, r return nil, errors.WithStack(ErrNotResumable.WithDebugf("Resumable ID from cookie could not be found in the datastore: %+v", err)) } else if err != nil { return nil, err + } else if container.ExpiresAt.Before(time.Now()) { + _ = x.SessionUnsetKey(w, r, m.d.ContinuityCookieManager(ctx), CookieName, name) + return nil, errors.WithStack(ErrNotResumable.WithDebugf("Resumable session has expired")) } return container, err diff --git a/continuity/manager_options_test.go b/continuity/manager_options_test.go index be2e9f73d4d0..8286f12e9e77 100644 --- a/continuity/manager_options_test.go +++ b/continuity/manager_options_test.go @@ -20,7 +20,7 @@ func TestManagerOptions(t *testing.T) { }{ { e: func(t *testing.T, actual *managerOptions) { - assert.EqualValues(t, time.Minute, actual.ttl) + assert.EqualValues(t, time.Minute*10, actual.ttl) }, }, { diff --git a/continuity/manager_test.go b/continuity/manager_test.go index 6790137faa80..8e71024d16cd 100644 --- a/continuity/manager_test.go +++ b/continuity/manager_test.go @@ -12,6 +12,7 @@ import ( "net/http/httptest" "strings" "testing" + "time" "github.com/ory/kratos/driver/config" @@ -181,6 +182,50 @@ func TestManager(t *testing.T) { assert.Contains(t, href, gjson.GetBytes(body, "name").String(), "%s", body) }) + t.Run("case=pause and use session with expiry", func(t *testing.T) { + cl := newClient() + + tc := &persisterTestCase{ + ro: []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{"bar"}), continuity.WithExpireInsteadOfDelete(time.Minute)}, + wo: []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{}), continuity.WithExpireInsteadOfDelete(time.Minute)}, + } + ts := newServer(t, p, tc) + genid := func() string { + return ts.URL + "/" + x.NewUUID().String() + } + + href := genid() + res, err := cl.Do(testhelpers.NewTestHTTPRequest(t, "PUT", href, nil)) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusNoContent, res.StatusCode) + + res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil)) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusOK, res.StatusCode) + + res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil)) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusOK, res.StatusCode) + + tc.ro = []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{"bar"}), continuity.WithExpireInsteadOfDelete(-time.Minute)} + tc.wo = []continuity.ManagerOption{continuity.WithPayload(&persisterTestPayload{""}), continuity.WithExpireInsteadOfDelete(-time.Minute)} + + res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil)) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + require.Equal(t, http.StatusOK, res.StatusCode) + + res, err = cl.Do(testhelpers.NewTestHTTPRequest(t, "GET", href, nil)) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, res.StatusCode) + body := ioutilx.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + assert.Contains(t, gjson.GetBytes(body, "error.reason").String(), continuity.ErrNotResumable.ReasonField) + }) + for k, tc := range []persisterTestCase{ {}, { diff --git a/continuity/persistence.go b/continuity/persistence.go index 2499abe59e21..cc912731fa87 100644 --- a/continuity/persistence.go +++ b/continuity/persistence.go @@ -18,5 +18,6 @@ type Persister interface { SaveContinuitySession(ctx context.Context, c *Container) error GetContinuitySession(ctx context.Context, id uuid.UUID) (*Container, error) DeleteContinuitySession(ctx context.Context, id uuid.UUID) error + SetContinuitySessionExpiry(ctx context.Context, id uuid.UUID, expiresAt time.Time) error DeleteExpiredContinuitySessions(ctx context.Context, deleteOlder time.Time, pageSize int) error } diff --git a/continuity/test/persistence.go b/continuity/test/persistence.go index cd6f61188c22..752ccca4d542 100644 --- a/continuity/test/persistence.go +++ b/continuity/test/persistence.go @@ -101,6 +101,30 @@ func TestPersister(ctx context.Context, p interface { }) }) + t.Run("case=set expiry", func(t *testing.T) { + // Create a new continuity session + expected := createContainer(t) + require.NoError(t, p.SaveContinuitySession(ctx, &expected)) + + // Set the expiry of the continuity session + newExpiry := time.Now().Add(48 * time.Hour).UTC().Truncate(time.Second) + require.NoError(t, p.SetContinuitySessionExpiry(ctx, expected.ID, newExpiry)) + + // Retrieve the continuity session + actual, err := p.GetContinuitySession(ctx, expected.ID) + require.NoError(t, err) + + // Check if the expiry has been updated + assert.EqualValues(t, newExpiry, actual.ExpiresAt) + + t.Run("can not update on another network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + newExpiry := time.Now().Add(12 * time.Hour).UTC().Truncate(time.Second) + err := p.SetContinuitySessionExpiry(ctx, expected.ID, newExpiry) + require.ErrorIs(t, err, sqlcon.ErrNoRows) + }) + }) + t.Run("case=cleanup", func(t *testing.T) { id := x.NewUUID() yesterday := time.Now().Add(-24 * time.Hour).UTC().Truncate(time.Second) diff --git a/contrib/quickstart/kratos/all-strategies/identity.schema.json b/contrib/quickstart/kratos/all-strategies/identity.schema.json new file mode 100644 index 000000000000..6915fbeff5d7 --- /dev/null +++ b/contrib/quickstart/kratos/all-strategies/identity.schema.json @@ -0,0 +1,57 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + }, + "passkey": { + "display_name": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + }, + "name": { + "type": "object", + "properties": { + "first": { + "title": "First Name", + "type": "string" + }, + "last": { + "title": "Last Name", + "type": "string" + } + } + } + }, + "required": ["email"], + "additionalProperties": false + } + } +} diff --git a/contrib/quickstart/kratos/all-strategies/kratos.yml b/contrib/quickstart/kratos/all-strategies/kratos.yml new file mode 100644 index 000000000000..7aa978e18365 --- /dev/null +++ b/contrib/quickstart/kratos/all-strategies/kratos.yml @@ -0,0 +1,120 @@ +version: v0.13.0 + +dsn: memory + +serve: + public: + base_url: http://localhost:4433/ + cors: + enabled: true + admin: + base_url: http://kratos:4434/ + +session: + whoami: + required_aal: aal1 + +selfservice: + default_browser_return_url: http://localhost:4455/ + allowed_return_urls: + - http://localhost:4455 + - http://localhost:19006/Callback + - exp://localhost:8081/--/Callback + + methods: + password: + enabled: true + webauthn: + enabled: true + config: + passwordless: true + rp: + display_name: Your Application name + # Set 'id' to the top-level domain. + id: localhost + # Set 'origin' to the exact URL of the page that prompts the user to use WebAuthn. You must include the scheme, host, and port. + origin: http://localhost:4455 + passkey: + enabled: true + config: + rp: + display_name: Your Application name + # Set 'id' to the top-level domain. + id: localhost + # Set 'origin' to the exact URL of the page that prompts the user to use WebAuthn. You must include the scheme, host, and port. + origins: + - http://localhost:4455 + + flows: + error: + ui_url: http://localhost:4455/error + + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 15m + required_aal: aal1 + + recovery: + enabled: true + ui_url: http://localhost:4455/recovery + use: code + + verification: + enabled: false + ui_url: http://localhost:4455/verification + use: code + after: + default_browser_return_url: http://localhost:4455/ + + logout: + after: + default_browser_return_url: http://localhost:4455/login + + login: + ui_url: http://localhost:4455/login + lifespan: 10m + + registration: + enable_legacy_one_step: false + lifespan: 10m + ui_url: http://localhost:4455/registration + after: + passkey: + hooks: + - hook: session + webauthn: + hooks: + - hook: session + password: + hooks: + - hook: session + - hook: show_verification_ui + +log: + level: debug + format: text + leak_sensitive_values: true + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + cipher: + - 32-LONG-SECRET-NOT-SECURE-AT-ALL + +ciphers: + algorithm: xchacha20-poly1305 + +hashers: + algorithm: bcrypt + bcrypt: + cost: 8 + +identity: + default_schema_id: default + schemas: + - id: default + url: file://./contrib/quickstart/kratos/all-strategies/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true diff --git a/contrib/quickstart/kratos/email-password/kratos.yml b/contrib/quickstart/kratos/email-password/kratos.yml index d95e0ef68715..5597a1adcdb0 100644 --- a/contrib/quickstart/kratos/email-password/kratos.yml +++ b/contrib/quickstart/kratos/email-password/kratos.yml @@ -11,7 +11,7 @@ serve: base_url: http://kratos:4434/ selfservice: - default_browser_return_url: http://127.0.0.1:4455/ + default_browser_return_url: http://127.0.0.1:4455/welcome allowed_return_urls: - http://127.0.0.1:4455 - http://localhost:19006/Callback @@ -50,7 +50,7 @@ selfservice: ui_url: http://127.0.0.1:4455/verification use: code after: - default_browser_return_url: http://127.0.0.1:4455/ + default_browser_return_url: http://127.0.0.1:4455/welcome logout: after: diff --git a/contrib/quickstart/kratos/passkey/identity.schema.json b/contrib/quickstart/kratos/passkey/identity.schema.json new file mode 100644 index 000000000000..5fabe0c722eb --- /dev/null +++ b/contrib/quickstart/kratos/passkey/identity.schema.json @@ -0,0 +1,34 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "Your E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "passkey": { + "display_name": true + } + } + } + } + }, + "required": ["email"], + "additionalProperties": false + } + } +} diff --git a/contrib/quickstart/kratos/passkey/kratos.yml b/contrib/quickstart/kratos/passkey/kratos.yml new file mode 100644 index 000000000000..776b17e3cab3 --- /dev/null +++ b/contrib/quickstart/kratos/passkey/kratos.yml @@ -0,0 +1,107 @@ +serve: + public: + base_url: http://localhost:4433/ + cors: + enabled: true + admin: + base_url: http://kratos:4434/ + +session: + whoami: + required_aal: aal1 + +selfservice: + default_browser_return_url: http://localhost:4455/welcome + allowed_return_urls: + - http://localhost:4455 + - http://localhost:19006/Callback + - exp://example.com/Callback + - https://www.ory.sh/ + - https://example.org/ + - https://www.example.org/ + methods: + link: + config: + lifespan: 1h + code: + config: + lifespan: 1h + totp: + enabled: true + config: + issuer: issuer.ory.sh + lookup_secret: + enabled: true + webauthn: + enabled: true + config: + passwordless: true + rp: + id: localhost + origins: + - http://localhost:4455 + display_name: Ory + passkey: + enabled: true + config: + rp: + id: localhost + origins: + - http://localhost:4455 + display_name: Ory + flows: + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 5m + required_aal: aal1 + logout: + after: + default_browser_return_url: http://localhost:4455/login + registration: + ui_url: http://localhost:4455/registration + after: + password: + hooks: + - hook: session + webauthn: + hooks: + - hook: session + passkey: + hooks: + - hook: session + login: + ui_url: http://localhost:4455/login + error: + ui_url: http://localhost:4455/error + verification: + ui_url: http://localhost:4455/verify + recovery: + ui_url: http://localhost:4455/recovery + +log: + level: debug + format: text + leak_sensitive_values: true + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + cipher: + - 32-LONG-SECRET-NOT-SECURE-AT-ALL + +ciphers: + algorithm: xchacha20-poly1305 + +hashers: + algorithm: bcrypt + bcrypt: + cost: 8 + +identity: + schemas: + - id: default + url: file://contrib/quickstart/kratos/passkey/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true diff --git a/contrib/quickstart/kratos/phone-password/kratos.yml b/contrib/quickstart/kratos/phone-password/kratos.yml index 88ee01bc84e6..5826e4af6eb9 100644 --- a/contrib/quickstart/kratos/phone-password/kratos.yml +++ b/contrib/quickstart/kratos/phone-password/kratos.yml @@ -11,7 +11,7 @@ serve: base_url: http://kratos:4434/ selfservice: - default_browser_return_url: http://127.0.0.1:4455/ + default_browser_return_url: http://127.0.0.1:4455/welcome allowed_return_urls: - http://127.0.0.1:4455 - http://localhost:19006/Callback @@ -50,7 +50,7 @@ selfservice: ui_url: http://127.0.0.1:4455/verification use: code after: - default_browser_return_url: http://127.0.0.1:4455/ + default_browser_return_url: http://127.0.0.1:4455/welcome logout: after: diff --git a/contrib/quickstart/kratos/webauthn/kratos.yml b/contrib/quickstart/kratos/webauthn/kratos.yml index e3560a4dbc77..62168855cd7a 100644 --- a/contrib/quickstart/kratos/webauthn/kratos.yml +++ b/contrib/quickstart/kratos/webauthn/kratos.yml @@ -11,7 +11,7 @@ serve: base_url: http://kratos:4434/ selfservice: - default_browser_return_url: http://localhost:4455/ + default_browser_return_url: http://localhost:4455/welcome allowed_return_urls: - http://localhost:4455 @@ -58,7 +58,7 @@ selfservice: ui_url: http://localhost:4455/verification use: code after: - default_browser_return_url: http://localhost:4455/ + default_browser_return_url: http://localhost:4455/welcome logout: after: diff --git a/corpx/faker.go b/corpx/faker.go index e8fc4b0e388f..ec54a252ab6b 100644 --- a/corpx/faker.go +++ b/corpx/faker.go @@ -17,8 +17,8 @@ import ( "github.com/ory/kratos/session" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" + "github.com/ory/x/pointerx" "github.com/ory/x/randx" - "github.com/ory/x/stringsx" ) var setup sync.Once @@ -31,13 +31,13 @@ func registerFakes() { _ = faker.SetRandomMapAndSliceSize(4) if err := faker.AddProvider("ptr_geo_location", func(v reflect.Value) (interface{}, error) { - return stringsx.GetPointer("Munich, Germany"), nil + return pointerx.Ptr("Munich, Germany"), nil }); err != nil { panic(err) } if err := faker.AddProvider("ptr_ipv4", func(v reflect.Value) (interface{}, error) { - return stringsx.GetPointer(faker.IPv4()), nil + return pointerx.Ptr(faker.IPv4()), nil }); err != nil { panic(err) } diff --git a/courier/courier_dispatcher.go b/courier/courier_dispatcher.go index 3d4835636206..47399369c3c3 100644 --- a/courier/courier_dispatcher.go +++ b/courier/courier_dispatcher.go @@ -10,11 +10,16 @@ import ( ) func (c *courier) DispatchMessage(ctx context.Context, msg Message) error { + logger := c.deps.Logger(). + WithField("message_id", msg.ID). + WithField("message_nid", msg.NID). + WithField("message_type", msg.Type). + WithField("message_template_type", msg.TemplateType). + WithField("message_subject", msg.Subject) + if err := c.deps.CourierPersister().IncrementMessageSendCount(ctx, msg.ID); err != nil { - c.deps.Logger(). + logger. WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). Error(`Unable to increment the message's "send_count" field`) return err } @@ -24,28 +29,21 @@ func (c *courier) DispatchMessage(ctx context.Context, msg Message) error { return errors.Errorf("message %s has unknown channel %q", msg.ID.String(), msg.Channel) } + logger = logger. + WithField("channel", channel.ID()) + if err := channel.Dispatch(ctx, msg); err != nil { return err } if err := c.deps.CourierPersister().SetMessageStatus(ctx, msg.ID, MessageStatusSent); err != nil { - c.deps.Logger(). + logger. WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). - WithField("channel", channel.ID()). Error(`Unable to set the message status to "sent".`) return err } - c.deps.Logger(). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). - WithField("message_type", msg.Type). - WithField("message_template_type", msg.TemplateType). - WithField("message_subject", msg.Subject). - WithField("channel", channel.ID()). - Debug("Courier sent out message.") + logger.Debug("Courier sent out message.") return nil } @@ -63,27 +61,28 @@ func (c *courier) DispatchQueue(ctx context.Context) error { } for k, msg := range messages { + logger := c.deps.Logger(). + WithField("message_id", msg.ID). + WithField("message_nid", msg.NID). + WithField("message_type", msg.Type). + WithField("message_template_type", msg.TemplateType). + WithField("message_subject", msg.Subject) + if msg.SendCount > maxRetries { if err := c.deps.CourierPersister().SetMessageStatus(ctx, msg.ID, MessageStatusAbandoned); err != nil { - c.deps.Logger(). + logger. WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). Error(`Unable to set the retried message's status to "abandoned".`) return err } // Skip the message - c.deps.Logger(). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). + logger. Warnf(`Message was abandoned because it did not deliver after %d attempts`, msg.SendCount) } else if err := c.DispatchMessage(ctx, msg); err != nil { if err := c.deps.CourierPersister().RecordDispatch(ctx, msg.ID, CourierMessageDispatchStatusFailed, err); err != nil { - c.deps.Logger(). + logger. WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). Error(`Unable to record failure log entry.`) if c.failOnDispatchError { return err @@ -92,10 +91,8 @@ func (c *courier) DispatchQueue(ctx context.Context) error { for _, replace := range messages[k:] { if err := c.deps.CourierPersister().SetMessageStatus(ctx, replace.ID, MessageStatusQueued); err != nil { - c.deps.Logger(). + logger. WithError(err). - WithField("message_id", replace.ID). - WithField("message_nid", replace.NID). Error(`Unable to reset the failed message's status to "queued".`) if c.failOnDispatchError { return err @@ -107,10 +104,8 @@ func (c *courier) DispatchQueue(ctx context.Context) error { return err } } else if err := c.deps.CourierPersister().RecordDispatch(ctx, msg.ID, CourierMessageDispatchStatusSuccess, nil); err != nil { - c.deps.Logger(). + logger. WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). Error(`Unable to record success log entry.`) // continue with execution, as the message was successfully dispatched } diff --git a/courier/handler_test.go b/courier/handler_test.go index 28a7ec55d8b2..f4ca48ba30d6 100644 --- a/courier/handler_test.go +++ b/courier/handler_test.go @@ -57,7 +57,7 @@ func TestHandler(t *testing.T) { conf.MustSet(ctx, config.ViperKeyAdminBaseURL, adminTS.URL) conf.MustSet(ctx, config.ViperKeyPublicBaseURL, mockServerURL.String()) - var get = func(t *testing.T, base *httptest.Server, href string, expectCode int) gjson.Result { + get := func(t *testing.T, base *httptest.Server, href string, expectCode int) gjson.Result { t.Helper() res, err := base.Client().Get(base.URL + href) require.NoError(t, err) @@ -69,7 +69,7 @@ func TestHandler(t *testing.T) { return gjson.ParseBytes(body) } - var getList = func(t *testing.T, tsName string, qs string) gjson.Result { + getList := func(t *testing.T, tsName string, qs string) gjson.Result { t.Helper() href := courier.AdminRouteListMessages + qs ts := adminTS @@ -109,12 +109,12 @@ func TestHandler(t *testing.T) { } require.NoError(t, reg.CourierPersister().AddMessage(context.Background(), &messages[i])) } - for i := 0; i < procCount; i++ { + for i := range procCount { require.NoError(t, reg.CourierPersister().SetMessageStatus(context.Background(), messages[i].ID, courier.MessageStatusProcessing)) } t.Run("paging", func(t *testing.T) { - t.Run("case=should return half of the messages", func(t *testing.T) { + t.Run("case=should return first half of the messages", func(t *testing.T) { qs := fmt.Sprintf("?page_token=%s&page_size=%d", defaultPageToken, msgCount/2) for _, tc := range tss { @@ -124,7 +124,7 @@ func TestHandler(t *testing.T) { }) } }) - t.Run("case=should return no message", func(t *testing.T) { + t.Run("case=should error with random page token", func(t *testing.T) { token := keysetpagination.MapPageToken{ "id": "1232", "created_at": time.Now().Add(time.Duration(-10) * time.Hour).Format("2006-01-02 15:04:05.99999-07:00"), @@ -133,8 +133,13 @@ func TestHandler(t *testing.T) { for _, tc := range tss { t.Run("endpoint="+tc.name, func(t *testing.T) { - parsed := getList(t, tc.name, qs) - assert.Len(t, parsed.Array(), 0) + path := courier.AdminRouteListMessages + qs + if tc.name == "public" { + path = x.AdminPrefix + path + } + resp, err := tc.s.Client().Get(tc.s.URL + path) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) }) } }) diff --git a/courier/http_channel.go b/courier/http_channel.go index 13e0a5792623..2e405fb22abe 100644 --- a/courier/http_channel.go +++ b/courier/http_channel.go @@ -8,6 +8,8 @@ import ( "encoding/json" "fmt" + "github.com/tidwall/gjson" + "github.com/pkg/errors" "github.com/ory/kratos/courier/template" @@ -89,13 +91,16 @@ func (c *httpChannel) Dispatch(ctx context.Context, msg Message) (err error) { return errors.WithStack(err) } + logger := c.d.Logger(). + WithField("http_server", gjson.GetBytes(c.requestConfig, "url").String()). + WithField("message_id", msg.ID). + WithField("message_nid", msg.NID). + WithField("message_type", msg.Type). + WithField("message_template_type", msg.TemplateType). + WithField("message_subject", msg.Subject) + if res.StatusCode >= 200 && res.StatusCode < 300 { - c.d.Logger(). - WithField("message_id", msg.ID). - WithField("message_type", msg.Type). - WithField("message_template_type", msg.TemplateType). - WithField("message_subject", msg.Subject). - Debug("Courier sent out mailer.") + logger.Debug("Courier sent out mailer.") return nil } @@ -103,11 +108,7 @@ func (c *httpChannel) Dispatch(ctx context.Context, msg Message) (err error) { "unable to dispatch mail delivery because upstream server replied with status code %d", res.StatusCode, ) - c.d.Logger(). - WithField("message_id", msg.ID). - WithField("message_type", msg.Type). - WithField("message_template_type", msg.TemplateType). - WithField("message_subject", msg.Subject). + logger. WithError(err). Error("sending mail via HTTP failed.") return errors.WithStack(err) diff --git a/courier/message.go b/courier/message.go index 2cc8ec5a1e32..4026e9c5f46a 100644 --- a/courier/message.go +++ b/courier/message.go @@ -220,7 +220,7 @@ func (m Message) DefaultPageToken() keysetpagination.PageToken { } } -func (m Message) TableName(ctx context.Context) string { +func (m Message) TableName(context.Context) string { return "courier_messages" } diff --git a/courier/sms_test.go b/courier/sms_test.go index a93a7974bf71..5dc727048be6 100644 --- a/courier/sms_test.go +++ b/courier/sms_test.go @@ -63,6 +63,7 @@ func TestQueueSMS(t *testing.T) { Body: body.Body, }) })) + t.Cleanup(srv.Close) requestConfig := fmt.Sprintf(`{ "url": "%s", @@ -112,8 +113,6 @@ func TestQueueSMS(t *testing.T) { assert.Equal(t, expected.To, message.To) assert.Equal(t, fmt.Sprintf("stub sms body %s\n", expected.Body), message.Body) } - - srv.Close() } func TestDisallowedInternalNetwork(t *testing.T) { diff --git a/courier/smtp.go b/courier/smtp.go index 8c7ef91fbe33..7895fd05f762 100644 --- a/courier/smtp.go +++ b/courier/smtp.go @@ -12,6 +12,8 @@ import ( "strconv" "time" + "github.com/pkg/errors" + "github.com/ory/herodot" "github.com/ory/kratos/driver/config" @@ -27,7 +29,7 @@ type SMTPClient struct { func NewSMTPClient(deps Dependencies, cfg *config.SMTPConfig) (*SMTPClient, error) { uri, err := url.Parse(cfg.ConnectionURI) if err != nil { - return nil, herodot.ErrInternalServerError.WithError(err.Error()) + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The SMTP connection URI is malformed. Please contact a system administrator.")) } var tlsCertificates []tls.Certificate @@ -64,6 +66,13 @@ func NewSMTPClient(deps Dependencies, cfg *config.SMTPConfig) (*SMTPClient, erro serverName = uri.Hostname() } + tlsConfig := &tls.Config{ + InsecureSkipVerify: sslSkipVerify, //#nosec G402 -- This is ok (and required!) because it is configurable and disabled by default. + Certificates: tlsCertificates, + ServerName: serverName, + MinVersion: tls.VersionTLS12, + } + // SMTP schemes // smtp: smtp clear text (with uri parameter) or with StartTLS (enforced by default) // smtps: smtp with implicit TLS (recommended way in 2021 to avoid StartTLS downgrade attacks @@ -73,14 +82,12 @@ func NewSMTPClient(deps Dependencies, cfg *config.SMTPConfig) (*SMTPClient, erro // Enforcing StartTLS by default for security best practices (config review, etc.) skipStartTLS, _ := strconv.ParseBool(uri.Query().Get("disable_starttls")) if !skipStartTLS { - //#nosec G402 -- This is ok (and required!) because it is configurable and disabled by default. - dialer.TLSConfig = &tls.Config{InsecureSkipVerify: sslSkipVerify, Certificates: tlsCertificates, ServerName: serverName} + dialer.TLSConfig = tlsConfig // Enforcing StartTLS dialer.StartTLSPolicy = gomail.MandatoryStartTLS } case "smtps": - //#nosec G402 -- This is ok (and required!) because it is configurable and disabled by default. - dialer.TLSConfig = &tls.Config{InsecureSkipVerify: sslSkipVerify, Certificates: tlsCertificates, ServerName: serverName} + dialer.TLSConfig = tlsConfig dialer.SSL = true } diff --git a/courier/smtp_channel.go b/courier/smtp_channel.go index a44719a351d6..15a685bcd7ad 100644 --- a/courier/smtp_channel.go +++ b/courier/smtp_channel.go @@ -65,6 +65,10 @@ func (c *SMTPChannel) Dispatch(ctx context.Context, msg Message) error { } } + if cfg == nil { + return errors.WithStack(herodot.ErrInternalServerError.WithErrorf("Courier tried to deliver an email but SMTP channel is misconfigured.")) + } + gm := mail.NewMessage() if cfg.FromName == "" { gm.SetHeader("From", cfg.FromAddress) @@ -82,31 +86,30 @@ func (c *SMTPChannel) Dispatch(ctx context.Context, msg Message) error { gm.SetBody("text/plain", msg.Body) + logger := c.d.Logger(). + WithField("smtp_server", fmt.Sprintf("%s:%d", c.smtpClient.Host, c.smtpClient.Port)). + WithField("smtp_ssl_enabled", c.smtpClient.SSL). + WithField("message_from", cfg.FromAddress). + WithField("message_id", msg.ID). + WithField("message_nid", msg.NID). + WithField("message_type", msg.Type). + WithField("message_template_type", msg.TemplateType). + WithField("message_subject", msg.Subject) + tmpl, err := c.newEmailTemplateFromMessage(c.d, msg) if err != nil { - c.d.Logger(). - WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). - Error(`Unable to get email template from message.`) + logger. + WithError(err).Error(`Unable to get email template from message.`) } else if htmlBody, err := tmpl.EmailBody(ctx); err != nil { - c.d.Logger(). - WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). - Error(`Unable to get email body from template.`) + logger. + WithError(err).Error(`Unable to get email body from template.`) } else { gm.AddAlternative("text/html", htmlBody) } - if err := c.smtpClient.DialAndSend(ctx, gm); err != nil { - c.d.Logger(). + if err := errors.WithStack(c.smtpClient.DialAndSend(ctx, gm)); err != nil { + logger. WithError(err). - WithField("smtp_server", fmt.Sprintf("%s:%d", c.smtpClient.Host, c.smtpClient.Port)). - WithField("smtp_ssl_enabled", c.smtpClient.SSL). - WithField("message_from", cfg.FromAddress). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). Error("Unable to send email using SMTP connection.") var protoErr *textproto.Error @@ -119,10 +122,8 @@ func (c *SMTPChannel) Dispatch(ctx context.Context, msg Message) error { // See https://en.wikipedia.org/wiki/List_of_SMTP_server_return_codes // If the SMTP server responds with 5xx, sending the message should not be retried (without changing something about the request) if err := c.d.CourierPersister().SetMessageStatus(ctx, msg.ID, MessageStatusAbandoned); err != nil { - c.d.Logger(). + logger. WithError(err). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). Error(`Unable to reset the retried message's status to "abandoned".`) return err } @@ -132,13 +133,7 @@ func (c *SMTPChannel) Dispatch(ctx context.Context, msg Message) error { WithError(err.Error()).WithReason("failed to send email via smtp")) } - c.d.Logger(). - WithField("message_id", msg.ID). - WithField("message_nid", msg.NID). - WithField("message_type", msg.Type). - WithField("message_template_type", msg.TemplateType). - WithField("message_subject", msg.Subject). - Debug("Courier sent out message.") + logger.Debug("Courier sent out message.") return nil } diff --git a/courier/smtp_test.go b/courier/smtp_test.go index 107ab2803447..28271d808179 100644 --- a/courier/smtp_test.go +++ b/courier/smtp_test.go @@ -35,6 +35,23 @@ import ( gomail "github.com/ory/mail/v3" ) +func TestNewSMTPClientPreventLeak(t *testing.T) { + // Test for https://hackerone.com/reports/2384028 + + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + + invalidURL := "sm<>t>p://f%oo::bar:baz@my-server:1234:122/" + conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, invalidURL) + channels, err := conf.CourierChannels(ctx) + require.NoError(t, err) + require.Len(t, channels, 1) + + _, err = courier.NewSMTPClient(reg, channels[0].SMTPConfig) + require.Error(t, err) + assert.NotContains(t, err.Error(), invalidURL) +} + func TestNewSMTP(t *testing.T) { ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) diff --git a/courier/template/email/login_code_valid.go b/courier/template/email/login_code_valid.go index f48ae6116118..b09f70f8625e 100644 --- a/courier/template/email/login_code_valid.go +++ b/courier/template/email/login_code_valid.go @@ -18,10 +18,11 @@ type ( model *LoginCodeValidModel } LoginCodeValidModel struct { - To string `json:"to"` - LoginCode string `json:"login_code"` - Identity map[string]interface{} `json:"identity"` - RequestURL string `json:"request_url"` + To string `json:"to"` + LoginCode string `json:"login_code"` + Identity map[string]interface{} `json:"identity"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/email/recovery_code_invalid.go b/courier/template/email/recovery_code_invalid.go index e2f648003271..c2852e01b12b 100644 --- a/courier/template/email/recovery_code_invalid.go +++ b/courier/template/email/recovery_code_invalid.go @@ -18,8 +18,9 @@ type ( model *RecoveryCodeInvalidModel } RecoveryCodeInvalidModel struct { - To string `json:"to"` - RequestURL string `json:"request_url"` + To string `json:"to"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/email/recovery_code_valid.go b/courier/template/email/recovery_code_valid.go index f9e2ad9ec20f..4e8992da3d1d 100644 --- a/courier/template/email/recovery_code_valid.go +++ b/courier/template/email/recovery_code_valid.go @@ -18,10 +18,11 @@ type ( model *RecoveryCodeValidModel } RecoveryCodeValidModel struct { - To string `json:"to"` - RecoveryCode string `json:"recovery_code"` - Identity map[string]interface{} `json:"identity"` - RequestURL string `json:"request_url"` + To string `json:"to"` + RecoveryCode string `json:"recovery_code"` + Identity map[string]interface{} `json:"identity"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/email/recovery_invalid.go b/courier/template/email/recovery_invalid.go index 38d70d44bdc9..81bae893de23 100644 --- a/courier/template/email/recovery_invalid.go +++ b/courier/template/email/recovery_invalid.go @@ -18,8 +18,9 @@ type ( m *RecoveryInvalidModel } RecoveryInvalidModel struct { - To string `json:"to"` - RequestURL string `json:"request_url"` + To string `json:"to"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/email/recovery_valid.go b/courier/template/email/recovery_valid.go index 18e4fde7bd66..f82a40b4f919 100644 --- a/courier/template/email/recovery_valid.go +++ b/courier/template/email/recovery_valid.go @@ -18,10 +18,11 @@ type ( m *RecoveryValidModel } RecoveryValidModel struct { - To string `json:"to"` - RecoveryURL string `json:"recovery_url"` - Identity map[string]interface{} `json:"identity"` - RequestURL string `json:"request_url"` + To string `json:"to"` + RecoveryURL string `json:"recovery_url"` + Identity map[string]interface{} `json:"identity"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/email/registration_code_valid.go b/courier/template/email/registration_code_valid.go index e63812b00b80..ec52362a8990 100644 --- a/courier/template/email/registration_code_valid.go +++ b/courier/template/email/registration_code_valid.go @@ -22,6 +22,7 @@ type ( Traits map[string]interface{} `json:"traits"` RegistrationCode string `json:"registration_code"` RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/email/verification_code_invalid.go b/courier/template/email/verification_code_invalid.go index 77aec0c04c3e..a7ebad3be8cb 100644 --- a/courier/template/email/verification_code_invalid.go +++ b/courier/template/email/verification_code_invalid.go @@ -18,8 +18,9 @@ type ( m *VerificationCodeInvalidModel } VerificationCodeInvalidModel struct { - To string `json:"to"` - RequestURL string `json:"request_url"` + To string `json:"to"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/email/verification_code_valid.go b/courier/template/email/verification_code_valid.go index 7cf5823b0524..bd2045a03d25 100644 --- a/courier/template/email/verification_code_valid.go +++ b/courier/template/email/verification_code_valid.go @@ -23,6 +23,7 @@ type ( VerificationCode string `json:"verification_code"` Identity map[string]interface{} `json:"identity"` RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/email/verification_invalid.go b/courier/template/email/verification_invalid.go index 7b0a776fa254..08485c92e94f 100644 --- a/courier/template/email/verification_invalid.go +++ b/courier/template/email/verification_invalid.go @@ -18,8 +18,9 @@ type ( m *VerificationInvalidModel } VerificationInvalidModel struct { - To string `json:"to"` - RequestURL string `json:"request_url"` + To string `json:"to"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/email/verification_valid.go b/courier/template/email/verification_valid.go index c04913953519..eb9578261d83 100644 --- a/courier/template/email/verification_valid.go +++ b/courier/template/email/verification_valid.go @@ -18,10 +18,11 @@ type ( m *VerificationValidModel } VerificationValidModel struct { - To string `json:"to"` - VerificationURL string `json:"verification_url"` - Identity map[string]interface{} `json:"identity"` - RequestURL string `json:"request_url"` + To string `json:"to"` + VerificationURL string `json:"verification_url"` + Identity map[string]interface{} `json:"identity"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/load_template.go b/courier/template/load_template.go index a34427949e95..db7ff61aea25 100644 --- a/courier/template/load_template.go +++ b/courier/template/load_template.go @@ -17,14 +17,14 @@ import ( "github.com/ory/x/fetcher" "github.com/Masterminds/sprig/v3" - lru "github.com/hashicorp/golang-lru" + lru "github.com/hashicorp/golang-lru/v2" "github.com/pkg/errors" ) //go:embed courier/builtin/templates/* var templates embed.FS -var Cache, _ = lru.New(16) +var Cache, _ = lru.New[string, Template](16) type Template interface { Execute(wr io.Writer, data interface{}) error @@ -36,7 +36,7 @@ type templateDependencies interface { func loadBuiltInTemplate(filesystem fs.FS, name string, html bool) (Template, error) { if t, found := Cache.Get(name); found { - return t.(Template), nil + return t, nil } file, err := filesystem.Open(name) @@ -77,18 +77,16 @@ func loadBuiltInTemplate(filesystem fs.FS, name string, html bool) (Template, er } func loadRemoteTemplate(ctx context.Context, d templateDependencies, url string, html bool) (t Template, err error) { - var b []byte if t, found := Cache.Get(url); found { - b = t.([]byte) - } else { - f := fetcher.NewFetcher(fetcher.WithClient(d.HTTPClient(ctx))) - bb, err := f.FetchContext(ctx, url) - if err != nil { - return nil, errors.WithStack(err) - } - b = bb.Bytes() - _ = Cache.Add(url, b) + return t, nil + } + + f := fetcher.NewFetcher(fetcher.WithClient(d.HTTPClient(ctx))) + bb, err := f.FetchContext(ctx, url) + if err != nil { + return nil, errors.WithStack(err) } + b := bb.Bytes() if html { t, err = htemplate.New(url).Funcs(sprig.HermeticHtmlFuncMap()).Parse(string(b)) @@ -101,13 +99,14 @@ func loadRemoteTemplate(ctx context.Context, d templateDependencies, url string, return nil, errors.WithStack(err) } } + Cache.Add(url, t) return t, nil } func loadTemplate(filesystem fs.FS, name, pattern string, html bool) (Template, error) { if t, found := Cache.Get(name); found { - return t.(Template), nil + return t, nil } matches, _ := fs.Glob(filesystem, name) diff --git a/courier/template/load_template_test.go b/courier/template/load_template_test.go index e6b043aa0c54..986745726952 100644 --- a/courier/template/load_template_test.go +++ b/courier/template/load_template_test.go @@ -21,7 +21,7 @@ import ( "github.com/ory/kratos/internal" "github.com/ory/x/fetcher" - lru "github.com/hashicorp/golang-lru" + lru "github.com/hashicorp/golang-lru/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -51,20 +51,20 @@ func TestLoadTextTemplate(t *testing.T) { }) t.Run("method=fallback to bundled", func(t *testing.T) { - template.Cache, _ = lru.New(16) // prevent Cache hit + template.Cache, _ = lru.New[string, template.Template](16) // prevent Cache hit actual := executeTextTemplate(t, "some/inexistent/dir", "test_stub/email.body.gotmpl", "", nil) assert.Contains(t, actual, "stub email") }) t.Run("method=with Sprig functions", func(t *testing.T) { - template.Cache, _ = lru.New(16) // prevent Cache hit - m := map[string]interface{}{"input": "hello world"} // create a simple model + template.Cache, _ = lru.New[string, template.Template](16) // prevent Cache hit + m := map[string]interface{}{"input": "hello world"} // create a simple model actual := executeTextTemplate(t, "courier/builtin/templates/test_stub", "email.body.sprig.gotmpl", "", m) assert.Contains(t, actual, "HelloWorld,HELLOWORLD") }) t.Run("method=sprig should not support non-hermetic", func(t *testing.T) { - template.Cache, _ = lru.New(16) + template.Cache, _ = lru.New[string, template.Template](16) ctx := context.Background() _, reg := internal.NewFastRegistryWithMocks(t) @@ -80,8 +80,8 @@ func TestLoadTextTemplate(t *testing.T) { }) t.Run("method=html with nested templates", func(t *testing.T) { - template.Cache, _ = lru.New(16) // prevent Cache hit - m := map[string]interface{}{"lang": "en_US"} // create a simple model + template.Cache, _ = lru.New[string, template.Template](16) // prevent Cache hit + m := map[string]interface{}{"lang": "en_US"} // create a simple model actual := executeHTMLTemplate(t, "courier/builtin/templates/test_stub", "email.body.html.gotmpl", "email.body.html*", m) assert.Contains(t, actual, "lang=en_US") }) @@ -182,7 +182,7 @@ func TestLoadTextTemplate(t *testing.T) { }) t.Run("case=disallowed resources", func(t *testing.T) { - require.NoError(t, reg.Config().GetProvider(ctx).Set(config.ViperKeyClientHTTPNoPrivateIPRanges, true)) + require.NoError(t, reg.Config().Set(ctx, config.ViperKeyClientHTTPNoPrivateIPRanges, true)) reg.HTTPClient(ctx).RetryMax = 1 reg.HTTPClient(ctx).RetryWaitMax = time.Millisecond diff --git a/courier/template/sms/login_code_valid.go b/courier/template/sms/login_code_valid.go index 439856accb8a..f365a6ba2800 100644 --- a/courier/template/sms/login_code_valid.go +++ b/courier/template/sms/login_code_valid.go @@ -17,10 +17,11 @@ type ( model *LoginCodeValidModel } LoginCodeValidModel struct { - To string `json:"to"` - LoginCode string `json:"login_code"` - Identity map[string]interface{} `json:"identity"` - RequestURL string `json:"request_url"` + To string `json:"to"` + LoginCode string `json:"login_code"` + Identity map[string]interface{} `json:"identity"` + RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/template/sms/verification_code.go b/courier/template/sms/verification_code.go index a367de9e584c..4204df0ac4c8 100644 --- a/courier/template/sms/verification_code.go +++ b/courier/template/sms/verification_code.go @@ -22,6 +22,7 @@ type ( VerificationCode string `json:"verification_code"` Identity map[string]interface{} `json:"identity"` RequestURL string `json:"request_url"` + TransientPayload map[string]interface{} `json:"transient_payload"` } ) diff --git a/courier/test/persistence.go b/courier/test/persistence.go index dddd8adb2cbf..3ef5529501e8 100644 --- a/courier/test/persistence.go +++ b/courier/test/persistence.go @@ -47,10 +47,14 @@ func TestPersister(ctx context.Context, newNetworkUnlessExisting NetworkWrapper, messages := make([]courier.Message, 5) t.Run("case=add messages to the queue", func(t *testing.T) { + t.Cleanup(func() { pop.SetNowFunc(time.Now) }) + now := time.Now() for k := range messages { + // We need to fake the time func to control the created_at column, which is the + // sort key for the messages. + pop.SetNowFunc(func() time.Time { return now.Add(time.Duration(k) * time.Hour) }) require.NoError(t, faker.FakeData(&messages[k])) require.NoError(t, p.AddMessage(ctx, &messages[k])) - time.Sleep(time.Second) // wait a bit so that the timestamp ordering works in MySQL. } }) diff --git a/driver/config/config.go b/driver/config/config.go index 7641d19d9478..bc60d584c50c 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -21,6 +21,7 @@ import ( "go.opentelemetry.io/otel/trace/noop" "github.com/ory/x/crdbx" + "github.com/ory/x/pointerx" "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" @@ -115,6 +116,8 @@ const ( ViperKeySessionTokenizerTemplates = "session.whoami.tokenizer.templates" ViperKeySessionWhoAmIAAL = "session.whoami.required_aal" ViperKeySessionWhoAmICaching = "feature_flags.cacheable_sessions" + ViperKeyFeatureFlagFasterSessionExtend = "feature_flags.faster_session_extend" + ViperKeySessionWhoAmICachingMaxAge = "feature_flags.cacheable_sessions_max_age" ViperKeyUseContinueWithTransitions = "feature_flags.use_continue_with_transitions" ViperKeySessionWhoAmIRefreshAllowed = "session.whoami.refresh_allowed" ViperKeySessionRefreshMinTimeLeft = "session.earliest_possible_extend" @@ -126,11 +129,14 @@ const ( ViperKeyURLsAllowedReturnToDomains = "selfservice.allowed_return_urls" ViperKeySelfServiceRegistrationEnabled = "selfservice.flows.registration.enabled" ViperKeySelfServiceRegistrationLoginHints = "selfservice.flows.registration.login_hints" + ViperKeySelfServiceRegistrationEnableLegacyOneStep = "selfservice.flows.registration.enable_legacy_one_step" ViperKeySelfServiceRegistrationUI = "selfservice.flows.registration.ui_url" ViperKeySelfServiceRegistrationRequestLifespan = "selfservice.flows.registration.lifespan" ViperKeySelfServiceRegistrationAfter = "selfservice.flows.registration.after" ViperKeySelfServiceRegistrationBeforeHooks = "selfservice.flows.registration.before.hooks" ViperKeySelfServiceLoginUI = "selfservice.flows.login.ui_url" + ViperKeySelfServiceLoginFlowStyle = "selfservice.flows.login.style" + ViperKeySecurityAccountEnumerationMitigate = "security.account_enumeration.mitigate" ViperKeySelfServiceLoginRequestLifespan = "selfservice.flows.login.lifespan" ViperKeySelfServiceLoginAfter = "selfservice.flows.login.after" ViperKeySelfServiceLoginBeforeHooks = "selfservice.flows.login.before.hooks" @@ -177,6 +183,7 @@ const ( ViperKeyLinkLifespan = "selfservice.methods.link.config.lifespan" ViperKeyLinkBaseURL = "selfservice.methods.link.config.base_url" ViperKeyCodeLifespan = "selfservice.methods.code.config.lifespan" + ViperKeyCodeConfigMissingCredentialFallbackEnabled = "selfservice.methods.code.config.missing_credential_fallback_enabled" ViperKeyPasswordHaveIBeenPwnedHost = "selfservice.methods.password.config.haveibeenpwned_host" ViperKeyPasswordHaveIBeenPwnedEnabled = "selfservice.methods.password.config.haveibeenpwned_enabled" ViperKeyPasswordMaxBreaches = "selfservice.methods.password.config.max_breaches" @@ -191,6 +198,10 @@ const ( ViperKeyWebAuthnRPOrigin = "selfservice.methods.webauthn.config.rp.origin" ViperKeyWebAuthnRPOrigins = "selfservice.methods.webauthn.config.rp.origins" ViperKeyWebAuthnPasswordless = "selfservice.methods.webauthn.config.passwordless" + ViperKeyPasskeyEnabled = "selfservice.methods.passkey.enabled" + ViperKeyPasskeyRPDisplayName = "selfservice.methods.passkey.config.rp.display_name" + ViperKeyPasskeyRPID = "selfservice.methods.passkey.config.rp.id" + ViperKeyPasskeyRPOrigins = "selfservice.methods.passkey.config.rp.origins" ViperKeyOAuth2ProviderURL = "oauth2_provider.url" ViperKeyOAuth2ProviderHeader = "oauth2_provider.headers" ViperKeyOAuth2ProviderOverrideReturnTo = "oauth2_provider.override_return_to" @@ -198,6 +209,7 @@ const ( ViperKeyClientHTTPPrivateIPExceptionURLs = "clients.http.private_ip_exception_urls" ViperKeyPreviewDefaultReadConsistencyLevel = "preview.default_read_consistency_level" ViperKeyVersion = "version" + ViperKeyPasswordMigrationHook = "selfservice.methods.password.config.migrate_hook" ViperKeyHasherLegacyFandomCost = "hashers.legacyfandom.cost" ViperKeyHasherLegacyFandomAESKey = "hashers.legacyfandom.key" ViperKeyIdentityCaseSensitiveIdentifier = "identity.case_sensitive_identifier" @@ -309,6 +321,10 @@ type ( Headers map[string]string `json:"headers" koanf:"headers"` LocalName string `json:"local_name" koanf:"local_name"` } + PasswordMigrationHook struct { + Enabled bool `json:"enabled" koanf:"enabled"` + Config json.RawMessage `json:"config" koanf:"config"` + } Config struct { l *logrusx.Logger p *configx.Provider @@ -386,13 +402,13 @@ func (s Schemas) FindSchemaByID(id string) (*Schema, error) { return nil, errors.Errorf("unable to find identity schema with id: %s", id) } -func MustNew(t testing.TB, l *logrusx.Logger, stdOutOrErr io.Writer, opts ...configx.OptionModifier) *Config { - p, err := New(context.TODO(), l, stdOutOrErr, opts...) +func MustNew(t testing.TB, l *logrusx.Logger, stdOutOrErr io.Writer, ctxer contextx.Contextualizer, opts ...configx.OptionModifier) *Config { + p, err := New(context.TODO(), l, stdOutOrErr, ctxer, opts...) require.NoError(t, err) return p } -func New(ctx context.Context, l *logrusx.Logger, stdOutOrErr io.Writer, opts ...configx.OptionModifier) (*Config, error) { +func New(ctx context.Context, l *logrusx.Logger, stdOutOrErr io.Writer, ctxer contextx.Contextualizer, opts ...configx.OptionModifier) (*Config, error) { var c *Config opts = append([]configx.OptionModifier{ @@ -421,7 +437,7 @@ func New(ctx context.Context, l *logrusx.Logger, stdOutOrErr io.Writer, opts ... l.UseConfig(p) - c = NewCustom(l, p, stdOutOrErr, &contextx.Default{}) + c = NewCustom(l, p, stdOutOrErr, ctxer) if !p.SkipValidation() { if err := c.validateIdentitySchemas(ctx); err != nil { @@ -499,7 +515,7 @@ func (p *Config) validateIdentitySchemas(ctx context.Context) error { } defer resource.Close() - schema, err := io.ReadAll(resource) + schema, err := io.ReadAll(io.LimitReader(resource, 1024*1024)) if err != nil { return errors.WithStack(err) } @@ -537,12 +553,14 @@ func (p *Config) cors(ctx context.Context, prefix string) (cors.Options, bool) { }) } -func (p *Config) Set(ctx context.Context, key string, value interface{}) error { - return p.GetProvider(ctx).Set(key, value) +// Deprecated: use context-based WithConfigValue instead +func (p *Config) Set(_ context.Context, key string, value interface{}) error { + return p.p.Set(key, value) } -func (p *Config) MustSet(ctx context.Context, key string, value interface{}) { - if err := p.GetProvider(ctx).Set(key, value); err != nil { +// Deprecated: use context-based WithConfigValue instead +func (p *Config) MustSet(_ context.Context, key string, value interface{}) { + if err := p.p.Set(key, value); err != nil { p.l.WithError(err).Fatalf("Unable to set \"%s\" to \"%s\".", key, value) } } @@ -722,6 +740,10 @@ func (p *Config) SelfServiceFlowRegistrationLoginHints(ctx context.Context) bool return p.GetProvider(ctx).Bool(ViperKeySelfServiceRegistrationLoginHints) } +func (p *Config) SelfServiceFlowRegistrationTwoSteps(ctx context.Context) bool { + return !p.GetProvider(ctx).BoolF(ViperKeySelfServiceRegistrationEnableLegacyOneStep, false) +} + func (p *Config) SelfServiceFlowVerificationEnabled(ctx context.Context) bool { return p.GetProvider(ctx).Bool(ViperKeySelfServiceVerificationEnabled) } @@ -763,7 +785,12 @@ func (p *Config) SelfServiceFlowSettingsBeforeHooks(ctx context.Context) []SelfS } func (p *Config) SelfServiceFlowRegistrationBeforeHooks(ctx context.Context) []SelfServiceHook { - return p.selfServiceHooks(ctx, ViperKeySelfServiceRegistrationBeforeHooks) + hooks := p.selfServiceHooks(ctx, ViperKeySelfServiceRegistrationBeforeHooks) + if p.SelfServiceFlowRegistrationTwoSteps(ctx) { + hooks = append(hooks, SelfServiceHook{"two_step_registration", json.RawMessage("{}")}) + } + + return hooks } func (p *Config) selfServiceHooks(ctx context.Context, key string) []SelfServiceHook { @@ -812,7 +839,7 @@ func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *Self var err error config, err = json.Marshal(pp.GetF(basePath+".config", config)) if err != nil { - p.l.WithError(err).Warn("Unable to marshal self service strategy configuration.") + p.l.WithError(err).Warn("Unable to marshal self-service strategy configuration.") config = json.RawMessage("{}") } @@ -820,6 +847,8 @@ func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *Self // we need to forcibly set these values here: defaultEnabled := false switch strategy { + case "identifier_first": + defaultEnabled = p.SelfServiceLoginFlowIdentifierFirstEnabled(ctx) case "code", "password", "profile": defaultEnabled = true } @@ -892,6 +921,10 @@ func (p *Config) SecretsSession(ctx context.Context) [][]byte { func (p *Config) SecretsCipher(ctx context.Context) [][32]byte { secrets := p.GetProvider(ctx).Strings(ViperKeySecretsCipher) + return ToCipherSecrets(secrets) +} + +func ToCipherSecrets(secrets []string) [][32]byte { var cleanSecrets []string for k := range secrets { if len(secrets[k]) == 32 { @@ -904,7 +937,7 @@ func (p *Config) SecretsCipher(ctx context.Context) [][32]byte { result := make([][32]byte, len(cleanSecrets)) for n, s := range secrets { for k, v := range []byte(s) { - result[n][k] = byte(v) + result[n][k] = v } } return result @@ -1082,7 +1115,7 @@ func (p *Config) SelfServiceFlowLogoutRedirectURL(ctx context.Context) *url.URL } func (p *Config) CourierEmailStrategy(ctx context.Context) string { - return p.GetProvider(ctx).String(ViperKeyCourierDeliveryStrategy) + return p.GetProvider(ctx).StringF(ViperKeyCourierDeliveryStrategy, "smtp") } func (p *Config) CourierEmailRequestConfig(ctx context.Context) json.RawMessage { @@ -1231,7 +1264,6 @@ func (p *Config) CourierChannels(ctx context.Context) (ccs []*CourierChannel, _ } } } - return ccs, nil } // load legacy configs @@ -1250,7 +1282,8 @@ func (p *Config) CourierChannels(ctx context.Context) (ccs []*CourierChannel, _ return nil, errors.WithStack(err) } } - return []*CourierChannel{&channel}, nil + ccs = append(ccs, &channel) + return ccs, nil } func splitUrlAndFragment(s string) (string, string) { @@ -1364,6 +1397,10 @@ func (p *Config) SelfServiceCodeMethodLifespan(ctx context.Context) time.Duratio return p.GetProvider(ctx).DurationF(ViperKeyCodeLifespan, time.Hour) } +func (p *Config) SelfServiceCodeMethodMissingCredentialFallbackEnabled(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeyCodeConfigMissingCredentialFallbackEnabled) +} + func (p *Config) DatabaseCleanupSleepTables(ctx context.Context) time.Duration { return p.GetProvider(ctx).Duration(ViperKeyDatabaseCleanupSleepTables) } @@ -1415,6 +1452,14 @@ func (p *Config) SessionWhoAmICaching(ctx context.Context) bool { return p.GetProvider(ctx).Bool(ViperKeySessionWhoAmICaching) } +func (p *Config) FeatureFlagFasterSessionExtend(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeyFeatureFlagFasterSessionExtend) +} + +func (p *Config) SessionWhoAmICachingMaxAge(ctx context.Context) time.Duration { + return p.GetProvider(ctx).DurationF(ViperKeySessionWhoAmICachingMaxAge, 0) +} + func (p *Config) UseContinueWithTransitions(ctx context.Context) bool { return p.GetProvider(ctx).Bool(ViperKeyUseContinueWithTransitions) } @@ -1516,6 +1561,24 @@ func (p *Config) WebAuthnConfig(ctx context.Context) *webauthn.Config { } } +func (p *Config) PasskeyConfig(ctx context.Context) *webauthn.Config { + scheme := p.SelfPublicURL(ctx).Scheme + id := p.GetProvider(ctx).String(ViperKeyPasskeyRPID) + origins := p.GetProvider(ctx).StringsF(ViperKeyPasskeyRPOrigins, []string{scheme + "://" + id}) + return &webauthn.Config{ + RPDisplayName: p.GetProvider(ctx).String(ViperKeyPasskeyRPDisplayName), + RPID: id, + RPOrigins: origins, + AuthenticatorSelection: protocol.AuthenticatorSelection{ + AuthenticatorAttachment: "platform", + RequireResidentKey: pointerx.Ptr(true), + ResidentKey: protocol.ResidentKeyRequirementRequired, + UserVerification: protocol.VerificationPreferred, + }, + EncodeUserIDAsString: false, + } +} + func (p *Config) HasherPasswordHashingAlgorithm(ctx context.Context) string { configValue := p.GetProvider(ctx).StringF(ViperKeyHasherAlgorithm, DefaultPasswordHashingAlgorithm) switch configValue { @@ -1622,3 +1685,29 @@ func (p *Config) TokenizeTemplate(ctx context.Context, key string) (_ *SessionTo func (p *Config) DefaultConsistencyLevel(ctx context.Context) crdbx.ConsistencyLevel { return crdbx.ConsistencyLevelFromString(p.GetProvider(ctx).String(ViperKeyPreviewDefaultReadConsistencyLevel)) } + +func (p *Config) PasswordMigrationHook(ctx context.Context) *PasswordMigrationHook { + hook := &PasswordMigrationHook{ + Enabled: p.GetProvider(ctx).BoolF(ViperKeyPasswordMigrationHook+".enabled", false), + } + if !hook.Enabled { + return hook + } + + hook.Config, _ = json.Marshal(p.GetProvider(ctx).Get(ViperKeyPasswordMigrationHook + ".config")) + + return hook +} + +func (p *Config) SelfServiceLoginFlowIdentifierFirstEnabled(ctx context.Context) bool { + switch p.GetProvider(ctx).String(ViperKeySelfServiceLoginFlowStyle) { + case "identifier_first": + return true + default: + return false + } +} + +func (p *Config) SecurityAccountEnumerationMitigate(ctx context.Context) bool { + return p.GetProvider(ctx).Bool(ViperKeySecurityAccountEnumerationMitigate) +} diff --git a/driver/config/config_test.go b/driver/config/config_test.go index 9b3dfe88470b..98baf438e9b1 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -15,10 +15,11 @@ import ( "os" "path/filepath" "strings" - "sync" "testing" "time" + "github.com/ory/x/contextx" + "github.com/ory/x/httpx" "github.com/ory/x/randx" @@ -52,6 +53,7 @@ func TestViperProvider(t *testing.T) { t.Run("suite=loaders", func(t *testing.T) { p := config.MustNew(t, logrusx.New("", ""), os.Stderr, + &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml"), configx.WithContext(ctx), ) @@ -90,6 +92,7 @@ func TestViperProvider(t *testing.T) { pWithFragments := config.MustNew(t, logrusx.New("", ""), os.Stderr, + &contextx.Default{}, configx.WithValues(map[string]interface{}{ config.ViperKeySelfServiceLoginUI: "http://test.kratos.ory.sh/#/login", config.ViperKeySelfServiceSettingsURL: "http://test.kratos.ory.sh/#/settings", @@ -106,6 +109,7 @@ func TestViperProvider(t *testing.T) { pWithRelativeFragments := config.MustNew(t, logrusx.New("", ""), os.Stderr, + &contextx.Default{}, configx.WithValues(map[string]interface{}{ config.ViperKeySelfServiceLoginUI: "/login", config.ViperKeySelfServiceSettingsURL: "/settings", @@ -131,6 +135,7 @@ func TestViperProvider(t *testing.T) { pWithIncorrectUrls := config.MustNew(t, logger, os.Stderr, + &contextx.Default{}, configx.WithValues(map[string]interface{}{ config.ViperKeySelfServiceLoginUI: v, }), @@ -162,6 +167,7 @@ func TestViperProvider(t *testing.T) { t.Run("group=identity", func(t *testing.T) { c := config.MustNew(t, logrusx.New("", ""), os.Stderr, + &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.mock.identities.yaml"), configx.SkipValidation()) @@ -199,7 +205,7 @@ func TestViperProvider(t *testing.T) { }, p.SecretsSession(ctx)) var cipherExpected [32]byte for k, v := range []byte("secret-thirty-two-character-long") { - cipherExpected[k] = byte(v) + cipherExpected[k] = v } assert.Equal(t, [][32]byte{ cipherExpected, @@ -212,7 +218,7 @@ func TestViperProvider(t *testing.T) { config string enabled bool }{ - {id: "password", enabled: true, config: `{"haveibeenpwned_host":"api.pwnedpasswords.com","haveibeenpwned_enabled":true,"ignore_network_errors":true,"max_breaches":0,"min_password_length":8,"max_password_length":1024,"identifier_similarity_check_enabled":true}`}, + {id: "password", enabled: true, config: `{"haveibeenpwned_host":"api.pwnedpasswords.com","haveibeenpwned_enabled":true,"ignore_network_errors":true,"max_breaches":0,"max_password_length":1024,"migrate_hook":{"config":{"emit_analytics_event":true,"method":"POST"},"enabled":false},"min_password_length":8,"identifier_similarity_check_enabled":true}`}, {id: "oidc", enabled: true, config: `{"providers":[{"client_id":"a","client_secret":"b","id":"github","provider":"github","mapper_url":"http://test.kratos.ory.sh/default-identity.schema.json"}]}`}, {id: "totp", enabled: true, config: `{"issuer":"issuer.ory.sh"}`}, } { @@ -228,12 +234,12 @@ func TestViperProvider(t *testing.T) { t.Run("hook=before", func(t *testing.T) { expHooks := []config.SelfServiceHook{ - {Name: "web_hook", Config: json.RawMessage(`{"method":"GET","url":"https://test.kratos.ory.sh/before_registration_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"https://test.kratos.ory.sh/before_registration_hook"}`)}, + {Name: "two_step_registration", Config: json.RawMessage(`{}`)}, } hooks := p.SelfServiceFlowRegistrationBeforeHooks(ctx) - require.Len(t, hooks, 1) assert.Equal(t, expHooks, hooks) // assert.EqualValues(t, "redirect", hook.Name) // assert.JSONEq(t, `{"allow_user_defined_redirect":false,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`, string(hook.Config)) @@ -247,7 +253,7 @@ func TestViperProvider(t *testing.T) { strategy: "password", hooks: []config.SelfServiceHook{ {Name: "session", Config: json.RawMessage(`{}`)}, - {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","method":"POST","url":"https://test.kratos.ory.sh/after_registration_password_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_registration_password_hook"}`)}, // {Name: "verify", Config: json.RawMessage(`{}`)}, // {Name: "redirect", Config: json.RawMessage(`{"allow_user_defined_redirect":false,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`)}, }, @@ -256,7 +262,7 @@ func TestViperProvider(t *testing.T) { strategy: "oidc", hooks: []config.SelfServiceHook{ // {Name: "verify", Config: json.RawMessage(`{}`)}, - {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","method":"GET","url":"https://test.kratos.ory.sh/after_registration_oidc_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"GET","url":"https://test.kratos.ory.sh/after_registration_oidc_hook"}`)}, {Name: "session", Config: json.RawMessage(`{}`)}, // {Name: "redirect", Config: json.RawMessage(`{"allow_user_defined_redirect":false,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`)}, }, @@ -264,7 +270,7 @@ func TestViperProvider(t *testing.T) { { strategy: config.HookGlobal, hooks: []config.SelfServiceHook{ - {Name: "web_hook", Config: json.RawMessage(`{"auth":{"config":{"in":"header","name":"My-Key","value":"My-Key-Value"},"type":"api_key"},"body":"/path/to/template.jsonnet","method":"POST","url":"https://test.kratos.ory.sh/after_registration_global_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"auth":{"config":{"in":"header","name":"My-Key","value":"My-Key-Value"},"type":"api_key"},"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_registration_global_hook"}`)}, }, }, } { @@ -284,7 +290,7 @@ func TestViperProvider(t *testing.T) { t.Run("hook=before", func(t *testing.T) { expHooks := []config.SelfServiceHook{ - {Name: "web_hook", Config: json.RawMessage(`{"method":"POST","url":"https://test.kratos.ory.sh/before_login_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/before_login_hook"}`)}, } hooks := p.SelfServiceFlowLoginBeforeHooks(ctx) @@ -304,20 +310,20 @@ func TestViperProvider(t *testing.T) { hooks: []config.SelfServiceHook{ {Name: "revoke_active_sessions", Config: json.RawMessage(`{}`)}, {Name: "require_verified_address", Config: json.RawMessage(`{}`)}, - {Name: "web_hook", Config: json.RawMessage(`{"auth":{"config":{"password":"super-secret","user":"test-user"},"type":"basic_auth"},"body":"/path/to/template.jsonnet","method":"POST","url":"https://test.kratos.ory.sh/after_login_password_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"auth":{"config":{"password":"super-secret","user":"test-user"},"type":"basic_auth"},"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_login_password_hook"}`)}, }, }, { strategy: "oidc", hooks: []config.SelfServiceHook{ - {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","method":"GET","url":"https://test.kratos.ory.sh/after_login_oidc_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"GET","url":"https://test.kratos.ory.sh/after_login_oidc_hook"}`)}, {Name: "revoke_active_sessions", Config: json.RawMessage(`{}`)}, }, }, { strategy: config.HookGlobal, hooks: []config.SelfServiceHook{ - {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","method":"POST","url":"https://test.kratos.ory.sh/after_login_global_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_login_global_hook"}`)}, }, }, } { @@ -339,19 +345,19 @@ func TestViperProvider(t *testing.T) { { strategy: "password", hooks: []config.SelfServiceHook{ - {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","method":"POST","url":"https://test.kratos.ory.sh/after_settings_password_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_settings_password_hook"}`)}, }, }, { strategy: "profile", hooks: []config.SelfServiceHook{ - {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","method":"POST","url":"https://test.kratos.ory.sh/after_settings_profile_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_settings_profile_hook"}`)}, }, }, { strategy: config.HookGlobal, hooks: []config.SelfServiceHook{ - {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","method":"POST","url":"https://test.kratos.ory.sh/after_settings_global_hook"}`)}, + {Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/after_settings_global_hook"}`)}, }, }, } { @@ -368,7 +374,7 @@ func TestViperProvider(t *testing.T) { assert.Equal(t, "http://test.kratos.ory.sh/recovery", p.SelfServiceFlowRecoveryUI(ctx).String()) hooks := p.SelfServiceFlowRecoveryAfterHooks(ctx, config.HookGlobal) - assert.Equal(t, []config.SelfServiceHook{{Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","method":"GET","url":"https://test.kratos.ory.sh/after_recovery_hook"}`)}}, hooks) + assert.Equal(t, []config.SelfServiceHook{{Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"GET","url":"https://test.kratos.ory.sh/after_recovery_hook"}`)}}, hooks) }) t.Run("method=verification", func(t *testing.T) { @@ -376,7 +382,7 @@ func TestViperProvider(t *testing.T) { assert.Equal(t, "http://test.kratos.ory.sh/verification", p.SelfServiceFlowVerificationUI(ctx).String()) hooks := p.SelfServiceFlowVerificationAfterHooks(ctx, config.HookGlobal) - assert.Equal(t, []config.SelfServiceHook{{Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","method":"GET","url":"https://test.kratos.ory.sh/after_verification_hook"}`)}}, hooks) + assert.Equal(t, []config.SelfServiceHook{{Name: "web_hook", Config: json.RawMessage(`{"body":"/path/to/template.jsonnet","headers":{"X-Custom-Header":"test"},"method":"GET","url":"https://test.kratos.ory.sh/after_verification_hook"}`)}}, hooks) }) t.Run("group=hashers", func(t *testing.T) { @@ -401,7 +407,7 @@ func TestViperProvider(t *testing.T) { func TestBcrypt(t *testing.T) { t.Parallel() ctx := context.Background() - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) require.NoError(t, p.Set(ctx, config.ViperKeyHasherBcryptCost, 4)) require.NoError(t, p.Set(ctx, "dev", false)) @@ -419,7 +425,7 @@ func TestProviderBaseURLs(t *testing.T) { machineHostname = "127.0.0.1" } - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, "https://"+machineHostname+":4433/", p.SelfPublicURL(ctx).String()) assert.Equal(t, "https://"+machineHostname+":4434/", p.SelfAdminURL(ctx).String()) @@ -447,7 +453,7 @@ func TestProviderSelfServiceLinkMethodBaseURL(t *testing.T) { machineHostname = "127.0.0.1" } - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, "https://"+machineHostname+":4433/", p.SelfServiceLinkMethodBaseURL(ctx).String()) p.MustSet(ctx, config.ViperKeyLinkBaseURL, "https://example.org/bar") @@ -457,7 +463,7 @@ func TestProviderSelfServiceLinkMethodBaseURL(t *testing.T) { func TestViperProvider_Secrets(t *testing.T) { t.Parallel() ctx := context.Background() - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) def := p.SecretsDefault(ctx) assert.NotEmpty(t, def) @@ -480,24 +486,25 @@ func TestViperProvider_Defaults(t *testing.T) { }{ { init: func() *config.Config { - return config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) }, }, { init: func() *config.Config { return config.MustNew(t, l, os.Stderr, + &contextx.Default{}, configx.WithConfigFiles("stub/.defaults.yml"), configx.SkipValidation()) }, }, { init: func() *config.Config { - return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("stub/.defaults-password.yml"), configx.SkipValidation()) + return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.defaults-password.yml"), configx.SkipValidation()) }, }, { init: func() *config.Config { - return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("../../test/e2e/profiles/recovery/.kratos.yml"), configx.SkipValidation()) + return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("../../test/e2e/profiles/recovery/.kratos.yml"), configx.SkipValidation()) }, expect: func(t *testing.T, p *config.Config) { assert.True(t, p.SelfServiceFlowRecoveryEnabled(ctx)) @@ -513,7 +520,7 @@ func TestViperProvider_Defaults(t *testing.T) { }, { init: func() *config.Config { - return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("../../test/e2e/profiles/verification/.kratos.yml"), configx.SkipValidation()) + return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("../../test/e2e/profiles/verification/.kratos.yml"), configx.SkipValidation()) }, expect: func(t *testing.T, p *config.Config) { assert.False(t, p.SelfServiceFlowRecoveryEnabled(ctx)) @@ -529,7 +536,7 @@ func TestViperProvider_Defaults(t *testing.T) { }, { init: func() *config.Config { - return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("../../test/e2e/profiles/oidc/.kratos.yml"), configx.SkipValidation()) + return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("../../test/e2e/profiles/oidc/.kratos.yml"), configx.SkipValidation()) }, expect: func(t *testing.T, p *config.Config) { assert.False(t, p.SelfServiceFlowRecoveryEnabled(ctx)) @@ -544,7 +551,7 @@ func TestViperProvider_Defaults(t *testing.T) { }, { init: func() *config.Config { - return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("stub/.kratos.notify-unknown-recipients.yml"), configx.SkipValidation()) + return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.notify-unknown-recipients.yml"), configx.SkipValidation()) }, expect: func(t *testing.T, p *config.Config) { assert.True(t, p.SelfServiceFlowRecoveryNotifyUnknownRecipients(ctx)) @@ -573,7 +580,7 @@ func TestViperProvider_Defaults(t *testing.T) { } t.Run("suite=ui_url", func(t *testing.T) { - p := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, "https://www.ory.sh/kratos/docs/fallback/login", p.SelfServiceFlowLoginUI(ctx).String()) assert.Equal(t, "https://www.ory.sh/kratos/docs/fallback/settings", p.SelfServiceFlowSettingsUI(ctx).String()) assert.Equal(t, "https://www.ory.sh/kratos/docs/fallback/registration", p.SelfServiceFlowRegistrationUI(ctx).String()) @@ -586,7 +593,7 @@ func TestViperProvider_ReturnTo(t *testing.T) { t.Parallel() ctx := context.Background() l := logrusx.New("", "") - p := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) p.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.sh/") assert.Equal(t, "https://www.ory.sh/", p.SelfServiceFlowVerificationReturnTo(ctx, urlx.ParseOrPanic("https://www.ory.sh/")).String()) @@ -603,7 +610,7 @@ func TestSession(t *testing.T) { t.Parallel() ctx := context.Background() l := logrusx.New("", "") - p := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, "ory_kratos_session", p.SessionName(ctx)) p.MustSet(ctx, config.ViperKeySessionName, "ory_session") @@ -634,7 +641,7 @@ func TestCookies(t *testing.T) { t.Parallel() ctx := context.Background() l := logrusx.New("", "") - p := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) t.Run("path", func(t *testing.T) { assert.Equal(t, "/", p.CookiePath(ctx)) @@ -681,14 +688,14 @@ func TestViperProvider_DSN(t *testing.T) { ctx := context.Background() t.Run("case=dsn: memory", func(t *testing.T) { - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) p.MustSet(ctx, config.ViperKeyDSN, "memory") assert.Equal(t, config.DefaultSQLiteMemoryDSN, p.DSN(ctx)) }) t.Run("case=dsn: not memory", func(t *testing.T) { - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) dsn := "sqlite://foo.db?_fk=true" p.MustSet(ctx, config.ViperKeyDSN, dsn) @@ -703,7 +710,7 @@ func TestViperProvider_DSN(t *testing.T) { l := logrusx.New("", "", logrusx.WithExitFunc(func(i int) { exitCode = i })) - p := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, dsn, p.DSN(ctx)) assert.NotEqual(t, 0, exitCode) @@ -719,7 +726,7 @@ func TestViperProvider_ParseURIOrFail(t *testing.T) { l := logrusx.New("", "", logrusx.WithExitFunc(func(i int) { exitCode = i })) - p := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) require.Zero(t, exitCode) const testKey = "testKeyNotUsedInTheRealSchema" @@ -773,7 +780,7 @@ func TestViperProvider_HaveIBeenPwned(t *testing.T) { t.Parallel() ctx := context.Background() - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) t.Run("case=hipb: host", func(t *testing.T) { p.MustSet(ctx, config.ViperKeyPasswordHaveIBeenPwnedHost, "foo.bar") assert.Equal(t, "foo.bar", p.PasswordPolicyConfig(ctx).HaveIBeenPwnedHost) @@ -811,7 +818,7 @@ func newTestConfig(t *testing.T) (_ *config.Config, _ *test.Hook, exited *bool) exited = new(bool) l.Logger.Hooks.Add(h) l.Logger.ExitFunc = func(code int) { *exited = true } - config := config.MustNew(t, l, os.Stderr, configx.SkipValidation()) + config := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation()) return config, h, exited } @@ -977,7 +984,7 @@ func TestIdentitySchemaValidation(t *testing.T) { l := logrusx.New("kratos-"+tmpConfig.Name(), "test") hook := test.NewLocal(l.Logger) - conf, err := config.New(ctx, l, os.Stderr, configx.WithConfigFiles(tmpConfig.Name())) + conf, err := config.New(ctx, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles(tmpConfig.Name())) assert.NoError(t, err) // clean the hooks since it will throw an event on first boot @@ -990,17 +997,15 @@ func TestIdentitySchemaValidation(t *testing.T) { } t.Run("case=skip invalid schema validation", func(t *testing.T) { - ctx := ctx - _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.invalid.identities.yaml"), configx.SkipValidation()) assert.NoError(t, err) }) t.Run("case=invalid schema should throw error", func(t *testing.T) { - ctx := ctx var stdErr bytes.Buffer - _, err := config.New(ctx, logrusx.New("", ""), &stdErr, + _, err := config.New(ctx, logrusx.New("", ""), &stdErr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.invalid.identities.yaml")) assert.Error(t, err) assert.Contains(t, err.Error(), "minimum 1 properties allowed, but found 0") @@ -1008,27 +1013,24 @@ func TestIdentitySchemaValidation(t *testing.T) { }) t.Run("case=must fail on loading unreachable schemas", func(t *testing.T) { - ctx = config.SetValidateIdentitySchemaResilientClientOptions(ctx, []httpx.ResilientOptions{ + // we make sure that the test runs into DNS issues instead of the context being canceled + ctx := config.SetValidateIdentitySchemaResilientClientOptions(ctx, []httpx.ResilientOptions{ httpx.ResilientClientWithMaxRetry(0), - httpx.ResilientClientWithConnectionTimeout(time.Nanosecond), + httpx.ResilientClientWithConnectionTimeout(5 * time.Second), }) - ctx, cancel := context.WithTimeout(ctx, time.Second*30) - t.Cleanup(cancel) - - err := make(chan error, 1) + err := make(chan error) go func(err chan error) { - _, e := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, e := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.mock.identities.yaml")) err <- e }(err) select { - case <-ctx.Done(): - panic("the test could not complete as the context timed out before the identity schema loader timed out") + case <-time.After(5 * time.Second): + t.Fatal("the test could not complete as the context timed out before the identity schema loader timed out") case e := <-err: - assert.Error(t, e) - assert.Contains(t, e.Error(), "Client.Timeout") + assert.ErrorContains(t, e, "no such host") } }) @@ -1047,35 +1049,23 @@ func TestIdentitySchemaValidation(t *testing.T) { t.Cleanup(cancel) _, hook, writeSchema := testWatch(t, ctx, &cobra.Command{}, identity) - - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - // Change the identity config to an invalid file - writeSchema(invalidIdentity.Identity.Schemas) - }() + writeSchema(invalidIdentity.Identity.Schemas) // There are a bunch of log messages beeing logged. We are looking for a specific one. - timeout := time.After(time.Millisecond * 500) - success := false - for !success { + for { for _, v := range hook.AllEntries() { s, err := v.String() require.NoError(t, err) - success = success || strings.Contains(s, "The changed identity schema configuration is invalid and could not be loaded.") + if strings.Contains(s, "The changed identity schema configuration is invalid and could not be loaded.") { + return + } } - select { case <-ctx.Done(): t.Fatal("the test could not complete as the context timed out before the file watcher updated") - case <-timeout: - t.Fatal("Expected log line was not encountered within specified timeout") default: // nothing } } - - wg.Wait() }) } }) @@ -1085,7 +1075,7 @@ func TestPasswordless(t *testing.T) { t.Parallel() ctx := context.Background() - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation(), configx.WithValue(config.ViperKeyWebAuthnPasswordless, true)) require.NoError(t, err) @@ -1100,7 +1090,7 @@ func TestPasswordlessCode(t *testing.T) { ctx := context.Background() - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation(), configx.WithValue(config.ViperKeySelfServiceStrategyConfig+".code", map[string]interface{}{ "passwordless_enabled": true, @@ -1117,7 +1107,7 @@ func TestChangeMinPasswordLength(t *testing.T) { t.Run("case=must fail on minimum password length below enforced minimum", func(t *testing.T) { ctx := context.Background() - _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml"), configx.WithValue(config.ViperKeyPasswordMinLength, 5)) @@ -1127,7 +1117,7 @@ func TestChangeMinPasswordLength(t *testing.T) { t.Run("case=must not fail on minimum password length above enforced minimum", func(t *testing.T) { ctx := context.Background() - _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml"), configx.WithValue(config.ViperKeyPasswordMinLength, 9)) @@ -1140,14 +1130,14 @@ func TestCourierEmailHTTP(t *testing.T) { ctx := context.Background() t.Run("case=configs set", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.email.http.yaml"), configx.SkipValidation()) assert.Equal(t, "http", conf.CourierEmailStrategy(ctx)) snapshotx.SnapshotT(t, conf.CourierEmailRequestConfig(ctx)) }) t.Run("case=defaults", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, "smtp", conf.CourierEmailStrategy(ctx)) }) @@ -1157,17 +1147,19 @@ func TestCourierChannels(t *testing.T) { t.Parallel() ctx := context.Background() t.Run("case=configs set", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.WithConfigFiles("stub/.kratos.courier.channels.yaml"), configx.SkipValidation()) + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.channels.yaml"), configx.SkipValidation()) channelConfig, err := conf.CourierChannels(ctx) require.NoError(t, err) - require.Len(t, channelConfig, 1) + require.Len(t, channelConfig, 2) assert.Equal(t, channelConfig[0].ID, "phone") assert.NotEmpty(t, channelConfig[0].RequestConfig) + assert.Equal(t, channelConfig[1].ID, "email") + assert.NotEmpty(t, channelConfig[1].SMTPConfig) }) t.Run("case=defaults", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) channelConfig, err := conf.CourierChannels(ctx) require.NoError(t, err) @@ -1186,7 +1178,7 @@ func TestCourierChannels(t *testing.T) { "smtp://username:pass%2Fword@email-smtp.eu-west-3.amazonaws.com:587/", } { t.Run("case="+tc, func(t *testing.T) { - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.WithValue(config.ViperKeyCourierSMTPURL, tc), configx.SkipValidation()) + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithValue(config.ViperKeyCourierSMTPURL, tc), configx.SkipValidation()) require.NoError(t, err) cs, err := conf.CourierChannels(ctx) require.NoError(t, err) @@ -1202,13 +1194,13 @@ func TestCourierMessageTTL(t *testing.T) { ctx := context.Background() t.Run("case=configs set", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.message_retries.yaml"), configx.SkipValidation()) assert.Equal(t, conf.CourierMessageRetries(ctx), 10) }) t.Run("case=defaults", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Equal(t, conf.CourierMessageRetries(ctx), 5) }) } @@ -1218,7 +1210,7 @@ func TestOAuth2Provider(t *testing.T) { ctx := context.Background() t.Run("case=configs set", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.oauth2_provider.yaml"), configx.SkipValidation()) assert.Equal(t, "https://oauth2_provider/", conf.OAuth2ProviderURL(ctx).String()) assert.Equal(t, http.Header{"Authorization": {"Basic"}}, conf.OAuth2ProviderHeader(ctx)) @@ -1226,7 +1218,7 @@ func TestOAuth2Provider(t *testing.T) { }) t.Run("case=defaults", func(t *testing.T) { - conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) assert.Empty(t, conf.OAuth2ProviderURL(ctx)) assert.Empty(t, conf.OAuth2ProviderHeader(ctx)) assert.False(t, conf.OAuth2ProviderOverrideReturnTo(ctx)) @@ -1238,7 +1230,7 @@ func TestWebauthn(t *testing.T) { ctx := context.Background() t.Run("case=multiple origins", func(t *testing.T) { - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.webauthn.origins.yaml")) require.NoError(t, err) webAuthnConfig := conf.WebAuthnConfig(ctx) @@ -1251,7 +1243,7 @@ func TestWebauthn(t *testing.T) { }) t.Run("case=one origin", func(t *testing.T) { - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.webauthn.origin.yaml")) require.NoError(t, err) webAuthnConfig := conf.WebAuthnConfig(ctx) @@ -1262,7 +1254,7 @@ func TestWebauthn(t *testing.T) { }) t.Run("case=id as origin", func(t *testing.T) { - conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml")) require.NoError(t, err) webAuthnConfig := conf.WebAuthnConfig(ctx) @@ -1273,7 +1265,7 @@ func TestWebauthn(t *testing.T) { }) t.Run("case=invalid", func(t *testing.T) { - _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.webauthn.invalid.yaml")) assert.Error(t, err) }) @@ -1284,19 +1276,19 @@ func TestCourierTemplatesConfig(t *testing.T) { ctx := context.Background() t.Run("case=partial template update allowed", func(t *testing.T) { - _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.remote.partial.templates.yaml")) assert.NoError(t, err) }) t.Run("case=load remote template with fallback template overrides path", func(t *testing.T) { - _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.remote.templates.yaml")) assert.NoError(t, err) }) t.Run("case=courier template helper", func(t *testing.T) { - c, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + c, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.remote.templates.yaml")) assert.NoError(t, err) @@ -1338,7 +1330,7 @@ func TestCleanup(t *testing.T) { t.Parallel() ctx := context.Background() - p := config.MustNew(t, logrusx.New("", ""), os.Stderr, + p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.yaml")) t.Run("group=cleanup config", func(t *testing.T) { diff --git a/driver/config/handler_test.go b/driver/config/handler_test.go index da84a5bc08d4..8c73a9319621 100644 --- a/driver/config/handler_test.go +++ b/driver/config/handler_test.go @@ -6,9 +6,10 @@ package config_test import ( "context" "io" - "net/http/httptest" "testing" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/julienschmidt/httprouter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -17,21 +18,32 @@ import ( "github.com/ory/kratos/internal" ) +type configProvider struct { + cfg *config.Config +} + +func (c *configProvider) Config() *config.Config { + return c.cfg +} + func TestNewConfigHashHandler(t *testing.T) { ctx := context.Background() - conf, reg := internal.NewFastRegistryWithMocks(t) + cfg := internal.NewConfigurationWithDefaults(t) router := httprouter.New() - config.NewConfigHashHandler(reg, router) - ts := httptest.NewServer(router) + config.NewConfigHashHandler(&configProvider{cfg: cfg}, router) + ts := confighelpers.NewConfigurableTestServer(router) t.Cleanup(ts.Close) - res, err := ts.Client().Get(ts.URL + "/health/config") + + // first request, get baseline hash + res, err := ts.Client(ctx).Get(ts.URL + "/health/config") require.NoError(t, err) defer res.Body.Close() require.Equal(t, 200, res.StatusCode) first, err := io.ReadAll(res.Body) require.NoError(t, err) - res, err = ts.Client().Get(ts.URL + "/health/config") + // second request, no config change + res, err = ts.Client(ctx).Get(ts.URL + "/health/config") require.NoError(t, err) defer res.Body.Close() require.Equal(t, 200, res.StatusCode) @@ -39,13 +51,21 @@ func TestNewConfigHashHandler(t *testing.T) { require.NoError(t, err) assert.Equal(t, first, second) - require.NoError(t, conf.Set(ctx, config.ViperKeySessionDomain, "foobar")) + // third request, with config change + res, err = ts.Client(confighelpers.WithConfigValue(ctx, config.ViperKeySessionDomain, "foobar")).Get(ts.URL + "/health/config") + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, 200, res.StatusCode) + third, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.NotEqual(t, first, third) - res, err = ts.Client().Get(ts.URL + "/health/config") + // fourth request, no config change + res, err = ts.Client(ctx).Get(ts.URL + "/health/config") require.NoError(t, err) defer res.Body.Close() require.Equal(t, 200, res.StatusCode) - second, err = io.ReadAll(res.Body) + fourth, err := io.ReadAll(res.Body) require.NoError(t, err) - assert.NotEqual(t, first, second) + assert.Equal(t, first, fourth) } diff --git a/driver/config/stub/.kratos.courier.channels.yaml b/driver/config/stub/.kratos.courier.channels.yaml index 9f4cdcd6de94..16b116ba276f 100644 --- a/driver/config/stub/.kratos.courier.channels.yaml +++ b/driver/config/stub/.kratos.courier.channels.yaml @@ -1,4 +1,6 @@ courier: + smtp: + connection_uri: smtp://username:password@smtp.example.com:587 channels: - id: phone request_config: diff --git a/driver/config/stub/.kratos.webauthn.invalid.yaml b/driver/config/stub/.kratos.webauthn.invalid.yaml index 01fc9f5adaad..a1230588c666 100644 --- a/driver/config/stub/.kratos.webauthn.invalid.yaml +++ b/driver/config/stub/.kratos.webauthn.invalid.yaml @@ -104,6 +104,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_recovery_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet verification: @@ -117,6 +119,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_verification_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet settings: @@ -132,6 +136,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet profile: hooks: @@ -139,12 +145,16 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_profile_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet hooks: - hook: web_hook config: url: https://test.kratos.ory.sh/after_settings_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet login: @@ -156,6 +166,8 @@ selfservice: config: url: https://test.kratos.ory.sh/before_login_hook method: POST + headers: + X-Custom-Header: test after: default_browser_return_url: https://self-service/login/return_to password: @@ -167,6 +179,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet auth: type: basic_auth @@ -179,6 +193,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_oidc_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet - hook: revoke_active_sessions hooks: @@ -186,6 +202,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet registration: @@ -198,6 +216,8 @@ selfservice: config: url: https://test.kratos.ory.sh/before_registration_hook method: GET + headers: + X-Custom-Header: test after: default_browser_return_url: https://self-service/registration/return_to password: @@ -207,12 +227,16 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet hooks: - hook: web_hook config: url: https://test.kratos.ory.sh/after_registration_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet auth: type: api_key @@ -227,5 +251,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_oidc_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet - hook: session diff --git a/driver/config/stub/.kratos.webauthn.origin.yaml b/driver/config/stub/.kratos.webauthn.origin.yaml index 1b2ff11e9d8f..95deec00910d 100644 --- a/driver/config/stub/.kratos.webauthn.origin.yaml +++ b/driver/config/stub/.kratos.webauthn.origin.yaml @@ -100,6 +100,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_recovery_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet verification: @@ -113,6 +115,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_verification_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet settings: @@ -128,6 +132,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet profile: hooks: @@ -135,12 +141,16 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_profile_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet hooks: - hook: web_hook config: url: https://test.kratos.ory.sh/after_settings_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet login: @@ -152,6 +162,8 @@ selfservice: config: url: https://test.kratos.ory.sh/before_login_hook method: POST + headers: + X-Custom-Header: test after: default_browser_return_url: https://self-service/login/return_to password: @@ -163,6 +175,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet auth: type: basic_auth @@ -175,6 +189,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_oidc_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet - hook: revoke_active_sessions hooks: @@ -182,6 +198,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet registration: @@ -194,6 +212,8 @@ selfservice: config: url: https://test.kratos.ory.sh/before_registration_hook method: GET + headers: + X-Custom-Header: test after: default_browser_return_url: https://self-service/registration/return_to password: @@ -203,12 +223,16 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet hooks: - hook: web_hook config: url: https://test.kratos.ory.sh/after_registration_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet auth: type: api_key @@ -223,5 +247,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_oidc_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet - hook: session diff --git a/driver/config/stub/.kratos.webauthn.origins.yaml b/driver/config/stub/.kratos.webauthn.origins.yaml index f9349c670ac0..9efeb34e0b02 100644 --- a/driver/config/stub/.kratos.webauthn.origins.yaml +++ b/driver/config/stub/.kratos.webauthn.origins.yaml @@ -103,6 +103,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_recovery_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet verification: @@ -116,6 +118,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_verification_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet settings: @@ -131,6 +135,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet profile: hooks: @@ -138,12 +144,16 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_profile_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet hooks: - hook: web_hook config: url: https://test.kratos.ory.sh/after_settings_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet login: @@ -155,6 +165,8 @@ selfservice: config: url: https://test.kratos.ory.sh/before_login_hook method: POST + headers: + X-Custom-Header: test after: default_browser_return_url: https://self-service/login/return_to password: @@ -166,6 +178,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet auth: type: basic_auth @@ -178,6 +192,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_oidc_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet - hook: revoke_active_sessions hooks: @@ -185,6 +201,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet registration: @@ -197,6 +215,8 @@ selfservice: config: url: https://test.kratos.ory.sh/before_registration_hook method: GET + headers: + X-Custom-Header: test after: default_browser_return_url: https://self-service/registration/return_to password: @@ -206,12 +226,16 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet hooks: - hook: web_hook config: url: https://test.kratos.ory.sh/after_registration_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet auth: type: api_key @@ -226,5 +250,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_oidc_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet - hook: session diff --git a/driver/config/stub/.kratos.yaml b/driver/config/stub/.kratos.yaml index 47cd68ecf9b4..dbbe603935c6 100644 --- a/driver/config/stub/.kratos.yaml +++ b/driver/config/stub/.kratos.yaml @@ -102,6 +102,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_recovery_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet verification: @@ -115,6 +117,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_verification_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet settings: @@ -130,6 +134,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet profile: hooks: @@ -137,12 +143,16 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_profile_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet hooks: - hook: web_hook config: url: https://test.kratos.ory.sh/after_settings_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet login: @@ -154,6 +164,8 @@ selfservice: config: url: https://test.kratos.ory.sh/before_login_hook method: POST + headers: + X-Custom-Header: test after: default_browser_return_url: https://self-service/login/return_to password: @@ -165,6 +177,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet auth: type: basic_auth @@ -177,6 +191,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_oidc_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet - hook: revoke_active_sessions hooks: @@ -184,6 +200,8 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet registration: @@ -196,6 +214,8 @@ selfservice: config: url: https://test.kratos.ory.sh/before_registration_hook method: GET + headers: + X-Custom-Header: test after: default_browser_return_url: https://self-service/registration/return_to password: @@ -205,12 +225,16 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_password_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet hooks: - hook: web_hook config: url: https://test.kratos.ory.sh/after_registration_global_hook method: POST + headers: + X-Custom-Header: test body: /path/to/template.jsonnet auth: type: api_key @@ -225,5 +249,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_oidc_hook method: GET + headers: + X-Custom-Header: test body: /path/to/template.jsonnet - hook: session diff --git a/driver/config/testhelpers/config.go b/driver/config/testhelpers/config.go new file mode 100644 index 000000000000..bf68772d9a3f --- /dev/null +++ b/driver/config/testhelpers/config.go @@ -0,0 +1,154 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "context" + "net/http" + "net/http/httptest" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/embedx" + "github.com/ory/x/configx" + "github.com/ory/x/contextx" +) + +type ( + TestConfigProvider struct { + contextx.Contextualizer + Options []configx.OptionModifier + } + contextKey int +) + +func (t *TestConfigProvider) NewProvider(ctx context.Context, opts ...configx.OptionModifier) (*configx.Provider, error) { + return configx.New(ctx, []byte(embedx.ConfigSchema), append(t.Options, opts...)...) +} + +func (t *TestConfigProvider) Config(ctx context.Context, config *configx.Provider) *configx.Provider { + config = t.Contextualizer.Config(ctx, config) + values, ok := ctx.Value(contextConfigKey).([]map[string]any) + if !ok { + return config + } + + opts := make([]configx.OptionModifier, 0, len(values)) + opts = append(opts, configx.WithValues(config.All())) + for _, v := range values { + opts = append(opts, configx.WithValues(v)) + } + config, err := t.NewProvider(ctx, opts...) + if err != nil { + // This is not production code. The provider is only used in tests. + panic(err) + } + return config +} + +const contextConfigKey contextKey = 1 + +var ( + _ contextx.Contextualizer = (*TestConfigProvider)(nil) +) + +func WithConfigValue(ctx context.Context, key string, value any) context.Context { + return WithConfigValues(ctx, map[string]any{key: value}) +} + +func WithConfigValues(ctx context.Context, setValues ...map[string]any) context.Context { + values, ok := ctx.Value(contextConfigKey).([]map[string]any) + if !ok { + values = make([]map[string]any, 0) + } + newValues := make([]map[string]any, len(values), len(values)+len(setValues)) + copy(newValues, values) + newValues = append(newValues, setValues...) + + return context.WithValue(ctx, contextConfigKey, newValues) +} + +type ConfigurableTestHandler struct { + configs map[uuid.UUID][]map[string]any + handler http.Handler +} + +func NewConfigurableTestHandler(h http.Handler) *ConfigurableTestHandler { + return &ConfigurableTestHandler{ + configs: make(map[uuid.UUID][]map[string]any), + handler: h, + } +} + +func (t *ConfigurableTestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + cID := r.Header.Get("Test-Config-Id") + if config, ok := t.configs[uuid.FromStringOrNil(cID)]; ok { + r = r.WithContext(WithConfigValues(r.Context(), config...)) + } + t.handler.ServeHTTP(w, r) +} + +func (t *ConfigurableTestHandler) RegisterConfig(config ...map[string]any) uuid.UUID { + id := uuid.Must(uuid.NewV4()) + t.configs[id] = config + return id +} + +func (t *ConfigurableTestHandler) UseConfig(r *http.Request, id uuid.UUID) *http.Request { + r.Header.Set("Test-Config-Id", id.String()) + return r +} + +func (t *ConfigurableTestHandler) UseConfigValues(r *http.Request, values ...map[string]any) *http.Request { + return t.UseConfig(r, t.RegisterConfig(values...)) +} + +type ConfigurableTestServer struct { + *httptest.Server + handler *ConfigurableTestHandler + transport http.RoundTripper +} + +func NewConfigurableTestServer(h http.Handler) *ConfigurableTestServer { + handler := NewConfigurableTestHandler(h) + server := httptest.NewServer(handler) + + t := server.Client().Transport + cts := &ConfigurableTestServer{ + handler: handler, + Server: server, + transport: t, + } + server.Client().Transport = cts + return cts +} + +func (t *ConfigurableTestServer) RoundTrip(r *http.Request) (*http.Response, error) { + config, ok := r.Context().Value(contextConfigKey).([]map[string]any) + if ok && config != nil { + r = t.handler.UseConfigValues(r, config...) + } + return t.transport.RoundTrip(r) +} + +type AutoContextClient struct { + *http.Client + transport http.RoundTripper + ctx context.Context +} + +func (t *ConfigurableTestServer) Client(ctx context.Context) *AutoContextClient { + baseClient := *t.Server.Client() + autoClient := &AutoContextClient{ + Client: &baseClient, + transport: t, + ctx: ctx, + } + baseClient.Transport = autoClient + return autoClient +} + +func (c *AutoContextClient) RoundTrip(r *http.Request) (*http.Response, error) { + return c.transport.RoundTrip(r.WithContext(c.ctx)) +} diff --git a/driver/factory.go b/driver/factory.go index e3470d3cffd9..da0dd5601e2b 100644 --- a/driver/factory.go +++ b/driver/factory.go @@ -38,7 +38,7 @@ func NewWithoutInit(ctx context.Context, stdOutOrErr io.Writer, sl *servicelocat c := newOptions(dOpts).config if c == nil { var err error - c, err = config.New(ctx, l, stdOutOrErr, opts...) + c, err = config.New(ctx, l, stdOutOrErr, sl.Contextualizer(), opts...) if err != nil { l.WithError(err).Error("Unable to instantiate configuration.") return nil, err diff --git a/driver/registry.go b/driver/registry.go index 32382f7dcb3e..c415c103abda 100644 --- a/driver/registry.go +++ b/driver/registry.go @@ -106,7 +106,7 @@ type Registry interface { courier.PersistenceProvider schema.HandlerProvider - schema.IdentityTraitsProvider + schema.IdentitySchemaProvider password2.ValidationProvider @@ -182,15 +182,16 @@ func NewRegistryFromDSN(ctx context.Context, c *config.Config, l *logrusx.Logger } type options struct { - skipNetworkInit bool - config *config.Config - replaceTracer func(*otelx.Tracer) *otelx.Tracer - inspect func(Registry) error - extraMigrations []fs.FS - replacementStrategies []NewStrategy - extraHooks map[string]func(config.SelfServiceHook) any - disableMigrationLogging bool - jsonnetPool jsonnetsecure.Pool + skipNetworkInit bool + config *config.Config + replaceTracer func(*otelx.Tracer) *otelx.Tracer + replaceIdentitySchemaProvider func(Registry) schema.IdentitySchemaProvider + inspect func(Registry) error + extraMigrations []fs.FS + replacementStrategies []NewStrategy + extraHooks map[string]func(config.SelfServiceHook) any + disableMigrationLogging bool + jsonnetPool jsonnetsecure.Pool } type RegistryOption func(*options) @@ -211,6 +212,12 @@ func WithConfig(config *config.Config) RegistryOption { } } +func WithIdentitySchemaProvider(f func(r Registry) schema.IdentitySchemaProvider) RegistryOption { + return func(o *options) { + o.replaceIdentitySchemaProvider = f + } +} + func ReplaceTracer(f func(*otelx.Tracer) *otelx.Tracer) RegistryOption { return func(o *options) { o.replaceTracer = f diff --git a/driver/registry_default.go b/driver/registry_default.go index b248b5d87d76..9d76167fc45d 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -12,6 +12,8 @@ import ( "testing" "time" + "github.com/ory/kratos/selfservice/strategy/idfirst" + "github.com/cenkalti/backoff" "github.com/dgraph-io/ristretto" "github.com/gobuffalo/pop/v6" @@ -45,6 +47,7 @@ import ( "github.com/ory/kratos/selfservice/strategy/link" "github.com/ory/kratos/selfservice/strategy/lookup" "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/selfservice/strategy/passkey" "github.com/ory/kratos/selfservice/strategy/password" "github.com/ory/kratos/selfservice/strategy/profile" "github.com/ory/kratos/selfservice/strategy/totp" @@ -92,13 +95,14 @@ type RegistryDefault struct { hookAddressVerifier *hook.AddressVerifier hookShowVerificationUI *hook.ShowVerificationUIHook hookCodeAddressVerifier *hook.CodeAddressVerifier + hookTwoStepRegistration *hook.TwoStepRegistration hookTotpSecretsDestroyer *hook.TotpSecretsDestroyer - credentialsHandler *credentials.Handler - - identityHandler *identity.Handler - identityValidator *identity.Validator - identityManager *identity.Manager + credentialsHandler *credentials.Handler + identityHandler *identity.Handler + identityValidator *identity.Validator + identityManager *identity.Manager + identitySchemaProvider schema.IdentitySchemaProvider courierHandler *courier.Handler @@ -326,8 +330,10 @@ func (m *RegistryDefault) selfServiceStrategies() []any { code.NewStrategy(m), link.NewStrategy(m), totp.NewStrategy(m), + passkey.NewStrategy(m), webauthn.NewStrategy(m), lookup.NewStrategy(m), + idfirst.NewStrategy(m), } } } @@ -336,6 +342,9 @@ func (m *RegistryDefault) selfServiceStrategies() []any { } func (m *RegistryDefault) strategyRegistrationEnabled(ctx context.Context, id string) bool { + if id == "profile" { + return m.Config().SelfServiceFlowRegistrationTwoSteps(ctx) + } return m.Config().SelfServiceStrategy(ctx, id).Enabled } @@ -380,6 +389,7 @@ nextStrategy: continue nextStrategy } } + if m.strategyLoginEnabled(ctx, s.ID().String()) { loginStrategies = append(loginStrategies, s) } @@ -483,12 +493,12 @@ func (m *RegistryDefault) Cipher(ctx context.Context) cipher.Cipher { if m.crypter == nil { switch m.c.CipherAlgorithm(ctx) { case "xchacha20-poly1305": - m.crypter = cipher.NewCryptChaCha20(m) + m.crypter = cipher.NewCryptChaCha20(m.Config()) case "aes": - m.crypter = cipher.NewCryptAES(m) + m.crypter = cipher.NewCryptAES(m.Config()) default: - m.crypter = cipher.NewNoop(m) - m.l.Logger.Warning("No encryption configuration found. Default algorithm (noop) will be use that mean sensitive data will be recorded in plaintext") + m.crypter = cipher.NewNoop() + m.l.Logger.Warning("No encryption configuration found. The default algorithm (noop) will be used, resulting in sensitive data being stored in plaintext") } } return m.crypter @@ -633,6 +643,7 @@ func (m *RegistryDefault) Init(ctx context.Context, ctxer contextx.Contextualize instrumentedsql.WithOmitArgs(), // don't risk leaking PII or secrets } } + if o.replaceTracer != nil { m.trc = o.replaceTracer(m.trc) } @@ -645,6 +656,10 @@ func (m *RegistryDefault) Init(ctx context.Context, ctxer contextx.Contextualize m.WithHooks(o.extraHooks) } + if o.replaceIdentitySchemaProvider != nil { + m.identitySchemaProvider = o.replaceIdentitySchemaProvider(m) + } + bc := backoff.NewExponentialBackOff() bc.MaxElapsedTime = time.Minute * 5 bc.Reset() @@ -799,6 +814,10 @@ func (m *RegistryDefault) LoginCodePersister() code.LoginCodePersister { return m.Persister() } +func (m *RegistryDefault) TransactionalPersisterProvider() x.TransactionalPersister { + return m.Persister() +} + func (m *RegistryDefault) Persister() persistence.Persister { return m.persister } diff --git a/driver/registry_default_hooks.go b/driver/registry_default_hooks.go index 3c037ddbf380..94e7da1f6f4b 100644 --- a/driver/registry_default_hooks.go +++ b/driver/registry_default_hooks.go @@ -50,6 +50,13 @@ func (m *RegistryDefault) HookShowVerificationUI() *hook.ShowVerificationUIHook return m.hookShowVerificationUI } +func (m *RegistryDefault) HookTwoStepRegistration() *hook.TwoStepRegistration { + if m.hookTwoStepRegistration == nil { + m.hookTwoStepRegistration = hook.NewTwoStepRegistration(m) + } + return m.hookTwoStepRegistration +} + // fandom-start func (m *RegistryDefault) HookTotpSecretsDestroyer() *hook.TotpSecretsDestroyer { @@ -66,10 +73,12 @@ func (m *RegistryDefault) WithHooks(hooks map[string]func(config.SelfServiceHook } func (m *RegistryDefault) getHooks(credentialsType string, configs []config.SelfServiceHook) (i []interface{}) { + var addSessionIssuer bool for _, h := range configs { switch h.Name { case hook.KeySessionIssuer: - i = append(i, m.HookSessionIssuer()) + // The session issuer hook always needs to come last. + addSessionIssuer = true case hook.KeySessionDestroyer: i = append(i, m.HookSessionDestroyer()) case hook.KeyWebHook: @@ -78,6 +87,10 @@ func (m *RegistryDefault) getHooks(credentialsType string, configs []config.Self i = append(i, m.HookAddressVerifier()) case hook.KeyVerificationUI: i = append(i, m.HookShowVerificationUI()) + case hook.KeyTwoStepRegistration: + i = append(i, m.HookTwoStepRegistration()) + case hook.KeyVerifier: + i = append(i, m.HookVerifier()) // fandom-start case hook.KeyTotpLookupSecretsDestroyer: i = append(i, m.HookTotpSecretsDestroyer()) @@ -100,6 +113,9 @@ func (m *RegistryDefault) getHooks(credentialsType string, configs []config.Self Errorf("A unknown hook was requested and can therefore not be used") } } + if addSessionIssuer { + i = append(i, m.HookSessionIssuer()) + } return i } diff --git a/driver/registry_default_schemas.go b/driver/registry_default_schemas.go index 9f68fbd2d86e..d3d61a3e7e35 100644 --- a/driver/registry_default_schemas.go +++ b/driver/registry_default_schemas.go @@ -5,32 +5,13 @@ package driver import ( "context" - "net/url" - - "github.com/pkg/errors" "github.com/ory/kratos/schema" ) -func (m *RegistryDefault) IdentityTraitsSchemas(ctx context.Context) (schema.Schemas, error) { - ms, err := m.Config().IdentityTraitsSchemas(ctx) - if err != nil { - return nil, err +func (m *RegistryDefault) IdentityTraitsSchemas(ctx context.Context) (schema.IdentitySchemaList, error) { + if m.identitySchemaProvider == nil { + m.identitySchemaProvider = schema.NewDefaultIdentityTraitsProvider(m) } - - var ss schema.Schemas - for _, s := range ms { - surl, err := url.Parse(s.URL) - if err != nil { - return nil, errors.WithStack(err) - } - - ss = append(ss, schema.Schema{ - ID: s.ID, - URL: surl, - RawURL: s.URL, - }) - } - - return ss, nil + return m.identitySchemaProvider.IdentityTraitsSchemas(ctx) } diff --git a/driver/registry_default_schemas_test.go b/driver/registry_default_schemas_test.go index 8d76c22bf491..aed6819a383b 100644 --- a/driver/registry_default_schemas_test.go +++ b/driver/registry_default_schemas_test.go @@ -39,7 +39,7 @@ func TestRegistryDefault_IdentityTraitsSchemas(t *testing.T) { ss, err := reg.IdentityTraitsSchemas(context.Background()) require.NoError(t, err) - assert.Equal(t, 2, len(ss)) + assert.Equal(t, 2, ss.Total()) assert.Contains(t, ss, defaultSchema) assert.Contains(t, ss, altSchema) } diff --git a/driver/registry_default_test.go b/driver/registry_default_test.go index c1a398326355..f6e347eb9302 100644 --- a/driver/registry_default_test.go +++ b/driver/registry_default_test.go @@ -10,6 +10,10 @@ import ( "os" "testing" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + + "github.com/ory/x/contextx" + "github.com/ory/kratos/selfservice/flow/recovery" "github.com/ory/kratos/selfservice/flow/verification" @@ -34,38 +38,40 @@ func TestDriverDefault_Hooks(t *testing.T) { t.Parallel() ctx := context.Background() + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + t.Run("type=verification", func(t *testing.T) { t.Parallel() // BEFORE hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []verification.PreHookExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []verification.PreHookExecutor { return nil }, }, { uc: "Two web_hooks are configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceVerificationBeforeHooks: []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []verification.PreHookExecutor { return []verification.PreHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), } }, }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PreVerificationHooks(ctx) @@ -79,6 +85,7 @@ func TestDriverDefault_Hooks(t *testing.T) { for _, tc := range []struct { uc string prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []verification.PostHookExecutor }{ { @@ -88,23 +95,24 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Multiple web_hooks configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceVerificationAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []verification.PostHookExecutor { return []verification.PostHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), } }, }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PostVerificationHooks(ctx) @@ -120,33 +128,33 @@ func TestDriverDefault_Hooks(t *testing.T) { // BEFORE hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []recovery.PreHookExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []recovery.PreHookExecutor { return nil }, }, { uc: "Two web_hooks are configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceRecoveryBeforeHooks: []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []recovery.PreHookExecutor { return []recovery.PreHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), } }, }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PreRecoveryHooks(ctx) @@ -159,33 +167,33 @@ func TestDriverDefault_Hooks(t *testing.T) { // AFTER hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []recovery.PostHookExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []recovery.PostHookExecutor { return nil }, }, { uc: "Multiple web_hooks configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceRecoveryAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []recovery.PostHookExecutor { return []recovery.PostHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), } }, }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PostRecoveryHooks(ctx) @@ -201,33 +209,38 @@ func TestDriverDefault_Hooks(t *testing.T) { // BEFORE hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []registration.PreHookExecutor }{ { - uc: "No hooks configured", - prep: func(conf *config.Config) {}, - expect: func(reg *driver.RegistryDefault) []registration.PreHookExecutor { return nil }, + uc: "No hooks configured", + expect: func(reg *driver.RegistryDefault) []registration.PreHookExecutor { + return []registration.PreHookExecutor{ + hook.NewTwoStepRegistration(reg), + } + }, }, { uc: "Two web_hooks are configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceRegistrationBeforeHooks: []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []registration.PreHookExecutor { return []registration.PreHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), + hook.NewTwoStepRegistration(reg), } }, }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PreRegistrationHooks(ctx) @@ -240,21 +253,20 @@ func TestDriverDefault_Hooks(t *testing.T) { // AFTER hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return nil }, }, { uc: "Only session hook configured for password strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".password.hooks", []map[string]interface{}{ + config: map[string]any{ + config.ViperKeySelfServiceVerificationEnabled: true, + config.ViperKeySelfServiceRegistrationAfter + ".password.hooks": []map[string]any{ {"hook": "session"}, - }) + }, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ @@ -265,62 +277,62 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "A session hook and a web_hook are configured for password strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".password.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "body": "bar"}}, + config: map[string]any{ + config.ViperKeySelfServiceVerificationEnabled: true, + config.ViperKeySelfServiceRegistrationAfter + ".password.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}}, {"hook": "session"}, - }) + }, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ hook.NewVerifier(reg), - hook.NewWebHook(reg, json.RawMessage(`{"body":"bar","method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"body":"bar","headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), hook.NewSessionIssuer(reg), } }, }, { uc: "Two web_hooks are configured on a global level", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceRegistrationAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), } }, }, { uc: "Hooks are configured on a global level, as well as on a strategy level", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".password.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "GET"}}, + config: map[string]any{ + config.ViperKeySelfServiceRegistrationAfter + ".password.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "session"}, - }) - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "POST"}}, - }) - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + }, + config.ViperKeySelfServiceRegistrationAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, + config.ViperKeySelfServiceVerificationEnabled: true, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ hook.NewVerifier(reg), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"foo"}`)), hook.NewSessionIssuer(reg), } }, }, { uc: "show_verification_ui is configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{ + config: map[string]any{ + config.ViperKeySelfServiceRegistrationAfter + ".hooks": []map[string]any{ {"hook": "show_verification_ui"}, - }) + }, }, expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return []registration.PostHookPostPersistExecutor{ @@ -330,8 +342,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PostRegistrationPostPersistHooks(ctx, identity.CredentialsTypePassword) @@ -434,33 +447,33 @@ func TestDriverDefault_Hooks(t *testing.T) { // BEFORE hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []login.PreHookExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []login.PreHookExecutor { return nil }, }, { uc: "Two web_hooks are configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceLoginBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceLoginBeforeHooks: []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []login.PreHookExecutor { return []login.PreHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), } }, }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PreLoginHooks(ctx) @@ -473,20 +486,19 @@ func TestDriverDefault_Hooks(t *testing.T) { // AFTER hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []login.PostHookExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return nil }, }, { uc: "Only revoke_active_sessions hook configured for password strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{ + config: map[string]any{ + config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{ {"hook": "revoke_active_sessions"}, - }) + }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ @@ -496,10 +508,10 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Only require_verified_address hook configured for password strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{ + config: map[string]any{ + config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{ {"hook": "require_verified_address"}, - }) + }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ @@ -509,16 +521,16 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "A revoke_active_sessions hook, require_verified_address hook and a web_hook are configured for password strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "body": "bar"}}, + config: map[string]any{ + config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}}, {"hook": "require_verified_address"}, {"hook": "revoke_active_sessions"}, - }) + }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"body":"bar","method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"body":"bar","headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), hook.NewAddressVerifier(), hook.NewSessionDestroyer(reg), } @@ -526,34 +538,34 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "Two web_hooks are configured on a global level", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceLoginAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), } }, }, { uc: "Hooks are configured on a global level, as well as on a strategy level", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "GET"}}, + config: map[string]any{ + config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, {"hook": "revoke_active_sessions"}, {"hook": "require_verified_address"}, - }) - conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - }) + }, + config.ViperKeySelfServiceLoginAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return []login.PostHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"foo"}`)), hook.NewSessionDestroyer(reg), hook.NewAddressVerifier(), } @@ -561,8 +573,9 @@ func TestDriverDefault_Hooks(t *testing.T) { }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PostLoginHooks(ctx, identity.CredentialsTypePassword) @@ -578,33 +591,33 @@ func TestDriverDefault_Hooks(t *testing.T) { // BEFORE hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []settings.PreHookExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []settings.PreHookExecutor { return nil }, }, { uc: "Two web_hooks are configured", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceSettingsBeforeHooks, []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceSettingsBeforeHooks: []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []settings.PreHookExecutor { return []settings.PreHookExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), } }, }, } { t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PreSettingsHooks(ctx) @@ -617,18 +630,17 @@ func TestDriverDefault_Hooks(t *testing.T) { // AFTER hooks for _, tc := range []struct { uc string - prep func(conf *config.Config) + config map[string]any expect func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor }{ { uc: "No hooks configured", - prep: func(conf *config.Config) {}, expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return nil }, }, { uc: "Only verify hook configured for the strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + config: map[string]any{ + config.ViperKeySelfServiceVerificationEnabled: true, // I think this is a bug as there is a hook named verify defined for both profile and password // strategies. Instead of using it, the code makes use of the property used above and which // is defined in an entirely different flow (verification). @@ -641,56 +653,57 @@ func TestDriverDefault_Hooks(t *testing.T) { }, { uc: "A verify hook and a web_hook are configured for profile strategy", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".profile.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "body": "bar"}}, - }) - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) + config: map[string]any{ + config.ViperKeySelfServiceSettingsAfter + ".profile.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"headers": []map[string]string{{"X-Custom-Header": "test"}}, "url": "foo", "method": "POST", "body": "bar"}}, + }, + config.ViperKeySelfServiceVerificationEnabled: true, }, expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return []settings.PostHookPostPersistExecutor{ hook.NewVerifier(reg), - hook.NewWebHook(reg, json.RawMessage(`{"body":"bar","method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"body":"bar","headers":[{"X-Custom-Header":"test"}],"method":"POST","url":"foo"}`)), } }, }, { uc: "Two web_hooks are configured on a global level", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET"}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceSettingsAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return []settings.PostHookPostPersistExecutor{ - hook.NewWebHook(reg, json.RawMessage(`{"method":"POST","url":"foo"}`)), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"bar"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"bar"}`)), } }, }, { uc: "Hooks are configured on a global level, as well as on a strategy level", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true) - conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".profile.hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "GET"}}, - }) - conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".hooks", []map[string]interface{}{ - {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST"}}, - }) + config: map[string]any{ + config.ViperKeySelfServiceVerificationEnabled: true, + config.ViperKeySelfServiceSettingsAfter + ".profile.hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, + config.ViperKeySelfServiceSettingsAfter + ".hooks": []map[string]any{ + {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}}, + }, }, expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return []settings.PostHookPostPersistExecutor{ hook.NewVerifier(reg), - hook.NewWebHook(reg, json.RawMessage(`{"method":"GET","url":"foo"}`)), + hook.NewWebHook(reg, json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"GET","url":"foo"}`)), } }, }, } { t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() + + ctx := confighelpers.WithConfigValues(ctx, tc.config) h := reg.PostSettingsPostPersistHooks(ctx, "profile") @@ -705,61 +718,64 @@ func TestDriverDefault_Hooks(t *testing.T) { func TestDriverDefault_Strategies(t *testing.T) { t.Parallel() ctx := context.Background() + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + t.Run("case=registration", func(t *testing.T) { t.Parallel() - for k, tc := range []struct { + for _, tc := range []struct { name string - prep func(conf *config.Config) + config map[string]any expect []string }{ { name: "no strategies", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", false) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, + expect: []string{"profile"}, }, { name: "only password", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, - expect: []string{"password"}, + expect: []string{"password", "profile"}, }, { name: "oidc and password", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".oidc.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, - expect: []string{"password", "oidc"}, + expect: []string{"password", "oidc", "profile"}, }, { name: "oidc, password and totp", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".oidc.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".totp.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, - expect: []string{"password", "oidc"}, + expect: []string{"password", "oidc", "profile"}, }, { name: "password and code", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", true) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": true, }, - expect: []string{"password", "code"}, + expect: []string{"password", "profile", "code"}, }, } { - t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Run(fmt.Sprintf("subcase=%s", tc.name), func(t *testing.T) { + t.Parallel() - s := reg.RegistrationStrategies(context.Background()) + ctx := confighelpers.WithConfigValues(ctx, tc.config) + s := reg.RegistrationStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { assert.Equal(t, e, s[k].ID().String()) @@ -770,68 +786,69 @@ func TestDriverDefault_Strategies(t *testing.T) { t.Run("case=login", func(t *testing.T) { t.Parallel() + for _, tc := range []struct { name string - prep func(conf *config.Config) + config map[string]any expect []string }{ { name: "no strategies", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", false) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, }, { name: "only password", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"password"}, }, { name: "oidc and password", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".oidc.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"password", "oidc"}, }, { name: "oidc, password and totp", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".oidc.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".totp.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, }, expect: []string{"password", "oidc", "totp"}, }, { name: "password and code", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", true) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": true, }, expect: []string{"password", "code"}, }, { name: "code is enabled if passwordless_enabled is true", - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", false) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", true) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".code.passwordless_enabled": true, }, expect: []string{"code"}, }, } { t.Run(fmt.Sprintf("run=%s", tc.name), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() - s := reg.LoginStrategies(context.Background()) + ctx := confighelpers.WithConfigValues(ctx, tc.config) + s := reg.LoginStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { assert.Equal(t, e, s[k].ID().String()) @@ -843,27 +860,28 @@ func TestDriverDefault_Strategies(t *testing.T) { t.Run("case=recovery", func(t *testing.T) { t.Parallel() for k, tc := range []struct { - prep func(conf *config.Config) + config map[string]any expect []string }{ { - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".link.enabled", false) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".link.enabled": false, }, }, { - prep: func(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".link.enabled", true) + config: map[string]any{ + config.ViperKeySelfServiceStrategyConfig + ".code.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".link.enabled": true, }, expect: []string{"code", "link"}, }, } { t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - tc.prep(conf) + t.Parallel() - s := reg.RecoveryStrategies(context.Background()) + ctx := confighelpers.WithConfigValues(ctx, tc.config) + + s := reg.RecoveryStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { assert.Equal(t, e, s[k].RecoveryStrategyID()) @@ -877,81 +895,55 @@ func TestDriverDefault_Strategies(t *testing.T) { l := logrusx.New("", "") for k, tc := range []struct { - prep func(t *testing.T) *config.Config - expect []string + configOptions []configx.OptionModifier + expect []string }{ { - prep: func(t *testing.T) *config.Config { - c := config.MustNew(t, l, - os.Stderr, - configx.WithValues(map[string]interface{}{ - config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, - config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, - config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": false, - config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": false, - }), - configx.SkipValidation()) - return c - }, + configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{ + config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": false, + })}, }, { - prep: func(t *testing.T) *config.Config { - c := config.MustNew(t, l, - os.Stderr, - configx.WithValues(map[string]interface{}{ - config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, - config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": true, - config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, - }), - configx.SkipValidation()) - return c - }, + configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{ + config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, + config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + })}, expect: []string{"profile"}, }, { - prep: func(t *testing.T) *config.Config { - c := config.MustNew(t, l, - os.Stderr, - configx.WithValues(map[string]interface{}{ - config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, - config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": true, - config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, - config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true, - }), - configx.SkipValidation()) - return c - }, + configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{ + config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, + config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": true, + config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false, + config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true, + })}, expect: []string{"profile", "totp"}, }, { - prep: func(t *testing.T) *config.Config { - return config.MustNew(t, l, - os.Stderr, - configx.WithValues(map[string]interface{}{ - config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, - }), - configx.SkipValidation()) - }, + configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{ + config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN, + })}, expect: []string{"password", "profile"}, }, { - prep: func(t *testing.T) *config.Config { - return config.MustNew(t, l, - os.Stderr, - configx.WithConfigFiles("../test/e2e/profiles/verification/.kratos.yml"), - configx.WithValue(config.ViperKeyDSN, config.DefaultSQLiteMemoryDSN), - configx.SkipValidation()) + configOptions: []configx.OptionModifier{ + configx.WithConfigFiles("../test/e2e/profiles/verification/.kratos.yml"), + configx.WithValue(config.ViperKeyDSN, config.DefaultSQLiteMemoryDSN), }, expect: []string{"password", "profile"}, }, } { t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { - conf := tc.prep(t) + conf := config.MustNew(t, l, os.Stderr, &contextx.Default{}, append(tc.configOptions, configx.SkipValidation())...) - reg, err := driver.NewRegistryFromDSN(ctx, conf, logrusx.New("", "")) + reg, err := driver.NewRegistryFromDSN(ctx, conf, l) require.NoError(t, err) - s := reg.SettingsStrategies(context.Background()) + s := reg.SettingsStrategies(ctx) require.Len(t, s, len(tc.expect)) for k, e := range tc.expect { @@ -967,7 +959,7 @@ func TestDefaultRegistry_AllStrategies(t *testing.T) { _, reg := internal.NewVeryFastRegistryWithoutDB(t) t.Run("case=all login strategies", func(t *testing.T) { - expects := []string{"password", "oidc", "code", "totp", "webauthn", "lookup_secret"} + expects := []string{"password", "oidc", "code", "totp", "passkey", "webauthn", "lookup_secret", "identifier_first"} s := reg.AllLoginStrategies() require.Len(t, s, len(expects)) for k, e := range expects { @@ -976,7 +968,7 @@ func TestDefaultRegistry_AllStrategies(t *testing.T) { }) t.Run("case=all registration strategies", func(t *testing.T) { - expects := []string{"password", "oidc", "code", "webauthn"} + expects := []string{"password", "oidc", "profile", "code", "passkey", "webauthn"} s := reg.AllRegistrationStrategies() require.Len(t, s, len(expects)) for k, e := range expects { @@ -985,7 +977,7 @@ func TestDefaultRegistry_AllStrategies(t *testing.T) { }) t.Run("case=all settings strategies", func(t *testing.T) { - expects := []string{"password", "oidc", "profile", "totp", "webauthn", "lookup_secret"} + expects := []string{"password", "oidc", "profile", "totp", "passkey", "webauthn", "lookup_secret"} s := reg.AllSettingsStrategies() require.Len(t, s, len(expects)) for k, e := range expects { @@ -1005,12 +997,16 @@ func TestDefaultRegistry_AllStrategies(t *testing.T) { func TestGetActiveRecoveryStrategy(t *testing.T) { t.Parallel() - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) + ctx := context.Background() + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + t.Run("returns error if active strategy is disabled", func(t *testing.T) { - conf.Set(context.Background(), "selfservice.methods.code.enabled", false) - conf.Set(context.Background(), config.ViperKeySelfServiceRecoveryUse, "code") + ctx := confighelpers.WithConfigValues(ctx, map[string]any{ + "selfservice.methods.code.enabled": false, + config.ViperKeySelfServiceRecoveryUse: "code", + }) - _, err := reg.GetActiveRecoveryStrategy(context.Background()) + _, err := reg.GetActiveRecoveryStrategy(ctx) require.Error(t, err) }) @@ -1019,10 +1015,12 @@ func TestGetActiveRecoveryStrategy(t *testing.T) { "code", "link", } { t.Run(fmt.Sprintf("strategy=%s", sID), func(t *testing.T) { - conf.Set(context.Background(), fmt.Sprintf("selfservice.methods.%s.enabled", sID), true) - conf.Set(context.Background(), config.ViperKeySelfServiceRecoveryUse, sID) + ctx := confighelpers.WithConfigValues(ctx, map[string]any{ + fmt.Sprintf("selfservice.methods.%s.enabled", sID): true, + config.ViperKeySelfServiceRecoveryUse: sID, + }) - s, err := reg.GetActiveRecoveryStrategy(context.Background()) + s, err := reg.GetActiveRecoveryStrategy(ctx) require.NoError(t, err) require.Equal(t, sID, s.RecoveryStrategyID()) }) @@ -1032,12 +1030,14 @@ func TestGetActiveRecoveryStrategy(t *testing.T) { func TestGetActiveVerificationStrategy(t *testing.T) { t.Parallel() - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) + ctx := context.Background() + _, reg := internal.NewVeryFastRegistryWithoutDB(t) t.Run("returns error if active strategy is disabled", func(t *testing.T) { - conf.Set(context.Background(), "selfservice.methods.code.enabled", false) - conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUse, "code") - - _, err := reg.GetActiveVerificationStrategy(context.Background()) + ctx := confighelpers.WithConfigValues(ctx, map[string]any{ + "selfservice.methods.code.enabled": false, + config.ViperKeySelfServiceVerificationUse: "code", + }) + _, err := reg.GetActiveVerificationStrategy(ctx) require.Error(t, err) }) @@ -1046,10 +1046,12 @@ func TestGetActiveVerificationStrategy(t *testing.T) { "code", "link", } { t.Run(fmt.Sprintf("strategy=%s", sID), func(t *testing.T) { - conf.Set(context.Background(), fmt.Sprintf("selfservice.methods.%s.enabled", sID), true) - conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUse, sID) + ctx := confighelpers.WithConfigValues(ctx, map[string]any{ + fmt.Sprintf("selfservice.methods.%s.enabled", sID): true, + config.ViperKeySelfServiceVerificationUse: sID, + }) - s, err := reg.GetActiveVerificationStrategy(context.Background()) + s, err := reg.GetActiveVerificationStrategy(ctx) require.NoError(t, err) require.Equal(t, sID, s.VerificationStrategyID()) }) diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 9403b582cea4..fb19d1ab46ec 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -75,6 +75,16 @@ "additionalProperties": false, "required": ["hook"] }, + "selfServiceVerificationHook": { + "type": "object", + "properties": { + "hook": { + "const": "verification" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, "selfServiceTotpSecretsDestroyerHook": { "type": "object", "properties": { @@ -275,6 +285,13 @@ "type": "string", "description": "The HTTP method to use (GET, POST, etc)." }, + "headers": { + "type": "object", + "description": "The HTTP headers that must be applied to the Web-Hook", + "additionalProperties": { + "type": "string" + } + }, "body": { "type": "string", "oneOf": [ @@ -457,7 +474,7 @@ }, "provider": { "title": "Provider", - "description": "Can be one of github, github-app, gitlab, generic, google, microsoft, discord, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon.", + "description": "Can be one of github, github-app, gitlab, generic, google, microsoft, discord, salesforce, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon.", "type": "string", "enum": [ "github", @@ -467,6 +484,7 @@ "google", "microsoft", "discord", + "salesforce", "slack", "facebook", "auth0", @@ -478,7 +496,9 @@ "dingtalk", "patreon", "linkedin", - "lark" + "linkedin_v2", + "lark", + "x" ], "examples": ["google"] }, @@ -589,6 +609,13 @@ "enum": ["id_token", "userinfo"], "default": "id_token", "examples": ["id_token", "userinfo"] + }, + "pkce": { + "title": "Proof Key for Code Exchange", + "description": "PKCE controls if the OpenID Connect OAuth2 flow should use PKCE (Proof Key for Code Exchange). IMPORTANT: If you set this to `force`, you must whitelist a different return URL for your OAuth2 client in the provider's configuration. Instead of /self-service/methods/oidc/callback/, you must use /self-service/methods/oidc/callback", + "type": "string", + "enum": ["auto", "never", "force"], + "default": "auto" } }, "additionalProperties": false, @@ -778,6 +805,12 @@ }, { "$ref": "#/definitions/selfServiceWebHook" + }, + { + "$ref": "#/definitions/selfServiceVerificationHook" + }, + { + "$ref": "#/definitions/selfServiceShowVerificationUIHook" } ] }, @@ -875,6 +908,9 @@ "webauthn": { "$ref": "#/definitions/selfServiceAfterSettingsAuthMethod" }, + "passkey": { + "$ref": "#/definitions/selfServiceAfterSettingsAuthMethod" + }, "lookup_secret": { "$ref": "#/definitions/selfServiceAfterSettingsAuthMethod" }, @@ -911,6 +947,9 @@ "webauthn": { "$ref": "#/definitions/selfServiceAfterDefaultLoginMethod" }, + "passkey": { + "$ref": "#/definitions/selfServiceAfterDefaultLoginMethod" + }, "oidc": { "$ref": "#/definitions/selfServiceAfterOIDCLoginMethod" }, @@ -936,6 +975,12 @@ { "$ref": "#/definitions/selfServiceRequireVerifiedAddressHook" }, + { + "$ref": "#/definitions/selfServiceVerificationHook" + }, + { + "$ref": "#/definitions/selfServiceShowVerificationUIHook" + }, { "$ref": "#/definitions/b2bSSOHook" } @@ -995,6 +1040,9 @@ "webauthn": { "$ref": "#/definitions/selfServiceAfterRegistrationMethod" }, + "passkey": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, "oidc": { "$ref": "#/definitions/selfServiceAfterRegistrationMethod" }, @@ -1280,6 +1328,12 @@ }, "after": { "$ref": "#/definitions/selfServiceAfterRegistration" + }, + "enable_legacy_one_step": { + "type": "boolean", + "title": "Disable two-step registration", + "description": "Two-step registration is a significantly improved sign up flow and recommended when using more than one sign up methods. To revert to one-step registration, set this to `true`.", + "default": false } } }, @@ -1301,6 +1355,13 @@ "default": "1h", "examples": ["1h", "1m", "1s"] }, + "style": { + "title": "Login Flow Style", + "description": "The style of the login flow. If set to `unified` the login flow will be a one-step process. If set to `identifier_first` (experimental!) the login flow will first ask for the identifier and then the credentials.", + "type": "string", + "enum": ["unified", "identifier_first"], + "default": "unified" + }, "before": { "$ref": "#/definitions/selfServiceBeforeLogin" }, @@ -1434,6 +1495,48 @@ "type": "object", "additionalProperties": false, "properties": { + "b2b": { + "title": "Single Sign-On for B2B", + "description": "Single Sign-On for B2B allows your customers to bring their own (workforce) identity server (e.g. OneLogin). This feature is not available in the open source licensed code.", + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": false, + "properties": { + "organizations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the organization.", + "format": "uuid", + "examples": ["00000000-0000-0000-0000-000000000000"] + }, + "label": { + "type": "string", + "description": "The label of the organization.", + "examples": ["ACME SSO"] + }, + "domains": { + "type": "array", + "items": { + "type": "string", + "format": "hostname", + "examples": ["my-app.com"], + "description": "If this domain matches the email's domain, this provider is shown." + } + } + } + } + } + } + } + }, + "additionalProperties": false + }, "profile": { "type": "object", "additionalProperties": false, @@ -1477,24 +1580,36 @@ }, "code": { "type": "object", - "additionalProperties": false, + "additionalProperties": true, "anyOf": [ { "properties": { - "passwordless_enabled": { "const": true }, - "mfa_enabled": { "const": false } + "passwordless_enabled": { + "const": true + }, + "mfa_enabled": { + "const": false + } } }, { "properties": { - "mfa_enabled": { "const": true }, - "passwordless_enabled": { "const": false } + "mfa_enabled": { + "const": true + }, + "passwordless_enabled": { + "const": false + } } }, { "properties": { - "mfa_enabled": { "const": false }, - "passwordless_enabled": { "const": false } + "mfa_enabled": { + "const": false + }, + "passwordless_enabled": { + "const": false + } } } ], @@ -1510,12 +1625,6 @@ "title": "Enables login flows code method to fulfil MFA requests", "default": false }, - "passwordless_login_fallback_enabled": { - "type": "boolean", - "title": "Passwordless Login Fallback Enabled", - "description": "This setting allows the code method to always login a user with code if they have registered with another authentication method such as password or social sign in.", - "default": false - }, "enabled": { "type": "boolean", "title": "Enables Code Method", @@ -1532,6 +1641,13 @@ "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$", "default": "1h", "examples": ["1h", "1m", "1s"] + }, + "missing_credential_fallback_enabled": { + "type": "boolean", + "title": "Enable Code OTP as a Fallback", + "description": "Enabling this allows users to sign in with the code method, even if their identity schema or their credentials are not set up to use the code method. If enabled, a verified address (such as an email) will be used to send the code to the user. Use with caution and only if actually needed.", + + "default": false } } } @@ -1596,6 +1712,61 @@ "description": "If set to false the password validation does not check for similarity between the password and the user identifier.", "type": "boolean", "default": true + }, + "migrate_hook": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Password Migration", + "description": "If set to true will enable password migration.", + "default": false + }, + "config": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "description": "The URL the password migration hook should call", + "format": "uri" + }, + "method": { + "type": "string", + "description": "The HTTP method to use (GET, POST, etc).", + "const": "POST", + "default": "POST" + }, + "headers": { + "type": "object", + "description": "The HTTP headers that must be applied to the password migration hook.", + "additionalProperties": { + "type": "string" + } + }, + "emit_analytics_event": { + "type": "boolean", + "default": true, + "description": "Emit tracing events for this hook on delivery or error" + }, + "auth": { + "type": "object", + "title": "Auth mechanisms", + "description": "Define which auth mechanism the Web-Hook should use", + "oneOf": [ + { + "$ref": "#/definitions/webHookAuthApiKeyProperties" + }, + { + "$ref": "#/definitions/webHookAuthBasicAuthProperties" + } + ] + }, + "additionalProperties": false + } + } + } } }, "additionalProperties": false @@ -1766,6 +1937,66 @@ "required": ["config"] } }, + "passkey": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables the Passkey method", + "default": false + }, + "config": { + "type": "object", + "title": "Passkey Configuration", + "properties": { + "rp": { + "title": "Relying Party (RP) Config", + "properties": { + "display_name": { + "type": "string", + "title": "Relying Party Display Name", + "description": "A name to help the user identify this RP.", + "examples": ["Ory Foundation"] + }, + "id": { + "type": "string", + "title": "Relying Party Identifier", + "description": "The id must be a subset of the domain currently in the browser.", + "examples": ["ory.sh"] + }, + "origins": { + "type": "array", + "title": "Relying Party Origins", + "description": "A list of explicit RP origins. If left empty, this defaults to either `origin` or `id`, prepended with the current protocol schema (HTTP or HTTPS).", + "items": { + "type": "string", + "examples": [ + "https://www.ory.sh", + "https://auth.ory.sh" + ] + } + } + }, + "type": "object", + "required": ["display_name", "id"] + } + }, + "additionalProperties": false + } + }, + "if": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + }, + "then": { + "required": ["config"] + } + }, "oidc": { "type": "object", "title": "Specify OpenID Connect and OAuth2 Configuration", @@ -2030,7 +2261,6 @@ "default": "localhost" } }, - "required": ["connection_uri"], "additionalProperties": false }, "sms": { @@ -2772,6 +3002,21 @@ } } }, + "security": { + "type": "object", + "properties": { + "account_enumeration": { + "type": "object", + "properties": { + "mitigate": { + "type": "boolean", + "default": false, + "description": "Mitigate account enumeration by making it harder to figure out if an identifier (email, phone number) exists or not. Enabling this setting degrades user experience. This setting does not mitigate all possible attack vectors yet." + } + } + } + } + }, "version": { "title": "The kratos version this config is written for.", "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", @@ -2850,20 +3095,45 @@ "description": "If enabled allows Ory Sessions to be cached. Only effective in the Ory Network.", "default": false }, + "cacheable_sessions_max_age": { + "title": "Set Ory Session Edge Caching maximum age", + "description": "Set how long Ory Sessions are cached on the edge. If unset, the session expiry will be used. Only effective in the Ory Network.", + "type": "string", + "pattern": "^([0-9]+(ns|us|ms|s|m|h))+$" + }, "use_continue_with_transitions": { "type": "boolean", "title": "Enable new flow transitions using `continue_with` items", "description": "If enabled allows new flow transitions using `continue_with` items.", "default": false + }, + "faster_session_extend": { + "type": "boolean", + "title": "Enable faster session extension", + "description": "If enabled allows faster session extension by skipping the session lookup. Disabling this feature will be deprecated in the future.", + "default": false } }, "additionalProperties": false }, "organizations": { "title": "Organizations", - "description": "Secifies which organizations are available. Only effective in the Ory Network.", + "description": "Please use selfservice.methods.b2b instead. This key will be removed. Only effective in the Ory Network.", "type": "array", "default": [] + }, + "enterprise": { + "title": "Enterprise features", + "description": "Specifies enterprise features. Only effective in the Ory Network or with a valid license.", + "type": "object", + "properties": { + "identity_schema_fallback_url_template": { + "type": "string", + "title": "Fallback URL template for identity schemas", + "description": "A fallback URL template used when looking up identity schemas." + } + }, + "additionalProperties": false } }, "allOf": [ diff --git a/embedx/identity_extension.schema.json b/embedx/identity_extension.schema.json index 6ad7765ef839..88c07b5f1153 100644 --- a/embedx/identity_extension.schema.json +++ b/embedx/identity_extension.schema.json @@ -30,6 +30,15 @@ } } }, + "passkey": { + "type": "object", + "additionalProperties": false, + "properties": { + "display_name": { + "type": "boolean" + } + } + }, "totp": { "type": "object", "additionalProperties": false, diff --git a/examples/go/identity/create/main.go b/examples/go/identity/create/main.go index 92f95769d054..be515e997131 100644 --- a/examples/go/identity/create/main.go +++ b/examples/go/identity/create/main.go @@ -19,7 +19,7 @@ var client = pkg.NewSDK("playground") func createIdentity() *ory.Identity { ctx := context.Background() - identity, res, err := client.IdentityApi.CreateIdentity(ctx). + identity, res, err := client.IdentityAPI.CreateIdentity(ctx). CreateIdentityBody(ory.CreateIdentityBody{ SchemaId: "default", Traits: map[string]interface{}{ diff --git a/examples/go/identity/delete/main.go b/examples/go/identity/delete/main.go index 999bc004dfa8..6857c2a3f6c5 100644 --- a/examples/go/identity/delete/main.go +++ b/examples/go/identity/delete/main.go @@ -20,7 +20,7 @@ func deleteIdentity() { identity := pkg.CreateIdentity(client) - res, err := client.IdentityApi.DeleteIdentity(ctx, identity.Id).Execute() + res, err := client.IdentityAPI.DeleteIdentity(ctx, identity.Id).Execute() pkg.SDKExitOnError(err, res) } diff --git a/examples/go/identity/get/main.go b/examples/go/identity/get/main.go index 865c72c3a1e7..dcd5cefc814d 100644 --- a/examples/go/identity/get/main.go +++ b/examples/go/identity/get/main.go @@ -19,7 +19,7 @@ func getIdentity() *ory.Identity { ctx := context.Background() created := pkg.CreateIdentity(client) - identity, res, err := client.IdentityApi.GetIdentity(ctx, created.Id).IncludeCredential([]string{"password"}).Execute() + identity, res, err := client.IdentityAPI.GetIdentity(ctx, created.Id).IncludeCredential([]string{"password"}).Execute() pkg.SDKExitOnError(err, res) return identity diff --git a/examples/go/identity/update/main.go b/examples/go/identity/update/main.go index a1d9062ad821..46b868f84b10 100644 --- a/examples/go/identity/update/main.go +++ b/examples/go/identity/update/main.go @@ -20,7 +20,7 @@ func updateIdentity() *ory.Identity { ctx := context.Background() toUpdate := pkg.CreateIdentity(client) - identity, res, err := client.IdentityApi.UpdateIdentity(ctx, toUpdate.Id).UpdateIdentityBody(ory.UpdateIdentityBody{ + identity, res, err := client.IdentityAPI.UpdateIdentity(ctx, toUpdate.Id).UpdateIdentityBody(ory.UpdateIdentityBody{ Traits: map[string]interface{}{ "email": "dev+not-" + x.NewUUID().String() + "@ory.sh", }, diff --git a/examples/go/pkg/common.go b/examples/go/pkg/common.go index edb8c17e2e19..f39725103e33 100644 --- a/examples/go/pkg/common.go +++ b/examples/go/pkg/common.go @@ -11,11 +11,9 @@ import ( "os" "testing" - "github.com/ory/kratos/x" - - "github.com/ory/kratos/internal/testhelpers" - ory "github.com/ory/client-go" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/x" ) func PrintJSONPretty(v interface{}) { diff --git a/examples/go/pkg/resources.go b/examples/go/pkg/resources.go index b931ef0fdda9..a6cd4326faed 100644 --- a/examples/go/pkg/resources.go +++ b/examples/go/pkg/resources.go @@ -32,11 +32,11 @@ func CreateIdentityWithSession(c *ory.APIClient, email, password string) (*ory.S } // Initialize a registration flow - flow, _, err := c.FrontendApi.CreateNativeRegistrationFlow(ctx).Execute() + flow, _, err := c.FrontendAPI.CreateNativeRegistrationFlow(ctx).Execute() ExitOnError(err) // Submit the registration flow - result, res, err := c.FrontendApi.UpdateRegistrationFlow(ctx).Flow(flow.Id).UpdateRegistrationFlowBody( + result, res, err := c.FrontendAPI.UpdateRegistrationFlow(ctx).Flow(flow.Id).UpdateRegistrationFlowBody( ory.UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody(&ory.UpdateRegistrationFlowWithPasswordMethod{ Method: "password", Password: password, @@ -56,7 +56,7 @@ func CreateIdentity(c *ory.APIClient) *ory.Identity { ctx := context.Background() email, _ := RandomCredentials() - identity, _, err := c.IdentityApi.CreateIdentity(ctx).CreateIdentityBody(ory.CreateIdentityBody{ + identity, _, err := c.IdentityAPI.CreateIdentity(ctx).CreateIdentityBody(ory.CreateIdentityBody{ SchemaId: "default", Traits: map[string]interface{}{ "email": email, diff --git a/examples/go/selfservice/error/main.go b/examples/go/selfservice/error/main.go index cdd31eec40aa..ca9eda53c86c 100644 --- a/examples/go/selfservice/error/main.go +++ b/examples/go/selfservice/error/main.go @@ -17,7 +17,7 @@ import ( var client = pkg.NewSDK("playground") func getError() *ory.FlowError { - e, res, err := client.FrontendApi.GetFlowError(context.Background()).Id("stub:500").Execute() + e, res, err := client.FrontendAPI.GetFlowError(context.Background()).Id("stub:500").Execute() pkg.SDKExitOnError(err, res) return e } diff --git a/examples/go/selfservice/login/main.go b/examples/go/selfservice/login/main.go index 23360c6e144b..c94f67e55bef 100644 --- a/examples/go/selfservice/login/main.go +++ b/examples/go/selfservice/login/main.go @@ -24,7 +24,7 @@ func performLogin() *ory.SuccessfulNativeLogin { _, _ = pkg.CreateIdentityWithSession(client, email, password) // Initialize the flow - flow, res, err := client.FrontendApi.CreateNativeLoginFlow(ctx).Execute() + flow, res, err := client.FrontendAPI.CreateNativeLoginFlow(ctx).Execute() pkg.SDKExitOnError(err, res) // If you want, print the flow here: @@ -32,7 +32,7 @@ func performLogin() *ory.SuccessfulNativeLogin { // pkg.PrintJSONPretty(flow) // Submit the form - result, res, err := client.FrontendApi.UpdateLoginFlow(ctx).Flow(flow.Id).UpdateLoginFlowBody( + result, res, err := client.FrontendAPI.UpdateLoginFlow(ctx).Flow(flow.Id).UpdateLoginFlowBody( ory.UpdateLoginFlowWithPasswordMethodAsUpdateLoginFlowBody(&ory.UpdateLoginFlowWithPasswordMethod{ Method: "password", Password: password, diff --git a/examples/go/selfservice/logout/main.go b/examples/go/selfservice/logout/main.go index 0ecb6eedd096..9d2bd02c2be2 100644 --- a/examples/go/selfservice/logout/main.go +++ b/examples/go/selfservice/logout/main.go @@ -23,7 +23,7 @@ func performLogout() { _, sessionToken := pkg.CreateIdentityWithSession(client, email, password) // Log out using session token - res, err := client.FrontendApi.PerformNativeLogout(context.Background()). + res, err := client.FrontendAPI.PerformNativeLogout(context.Background()). PerformNativeLogoutBody(ory.PerformNativeLogoutBody{SessionToken: sessionToken}).Execute() pkg.SDKExitOnError(err, res) } diff --git a/examples/go/selfservice/recovery/main.go b/examples/go/selfservice/recovery/main.go index 41011b9236a0..790d5dc91ee2 100644 --- a/examples/go/selfservice/recovery/main.go +++ b/examples/go/selfservice/recovery/main.go @@ -20,7 +20,7 @@ func performRecovery(email string) *ory.RecoveryFlow { ctx := context.Background() // Initialize the flow - flow, res, err := client.FrontendApi.CreateNativeRecoveryFlow(ctx).Execute() + flow, res, err := client.FrontendAPI.CreateNativeRecoveryFlow(ctx).Execute() pkg.SDKExitOnError(err, res) // If you want, print the flow here: @@ -28,7 +28,7 @@ func performRecovery(email string) *ory.RecoveryFlow { // pkg.PrintJSONPretty(flow) // Submit the form - afterSubmit, res, err := client.FrontendApi.UpdateRecoveryFlow(ctx).Flow(flow.Id). + afterSubmit, res, err := client.FrontendAPI.UpdateRecoveryFlow(ctx).Flow(flow.Id). UpdateRecoveryFlowBody(ory.UpdateRecoveryFlowWithLinkMethodAsUpdateRecoveryFlowBody(&ory.UpdateRecoveryFlowWithLinkMethod{ Email: email, Method: "link", diff --git a/examples/go/selfservice/registration/main.go b/examples/go/selfservice/registration/main.go index a038320ffa76..a51e0e210c62 100644 --- a/examples/go/selfservice/registration/main.go +++ b/examples/go/selfservice/registration/main.go @@ -20,7 +20,7 @@ func initRegistration() *ory.SuccessfulNativeRegistration { ctx := context.Background() // Initialize the flow - flow, res, err := client.FrontendApi.CreateNativeRegistrationFlow(ctx).Execute() + flow, res, err := client.FrontendAPI.CreateNativeRegistrationFlow(ctx).Execute() pkg.SDKExitOnError(err, res) // If you want, print the flow here: @@ -30,7 +30,7 @@ func initRegistration() *ory.SuccessfulNativeRegistration { email, password := pkg.RandomCredentials() // Submit the registration flow - result, res, err := client.FrontendApi.UpdateRegistrationFlow(ctx).Flow(flow.Id).UpdateRegistrationFlowBody( + result, res, err := client.FrontendAPI.UpdateRegistrationFlow(ctx).Flow(flow.Id).UpdateRegistrationFlowBody( ory.UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody(&ory.UpdateRegistrationFlowWithPasswordMethod{ Method: "password", Password: password, diff --git a/examples/go/selfservice/settings/main.go b/examples/go/selfservice/settings/main.go index 7311465a245a..725b3d8f836d 100644 --- a/examples/go/selfservice/settings/main.go +++ b/examples/go/selfservice/settings/main.go @@ -22,7 +22,7 @@ func initFlow(email, password string) (string, *ory.SettingsFlow) { // Create a temporary user _, sessionToken := pkg.CreateIdentityWithSession(client, email, password) - flow, res, err := client.FrontendApi.CreateNativeSettingsFlow(context.Background()).XSessionToken(sessionToken).Execute() + flow, res, err := client.FrontendAPI.CreateNativeSettingsFlow(context.Background()).XSessionToken(sessionToken).Execute() pkg.SDKExitOnError(err, res) // If you want, print the flow here: @@ -36,7 +36,7 @@ func changePassword(email, password string) *ory.SettingsFlow { sessionToken, flow := initFlow(email, password) // Submit the form - result, res, err := client.FrontendApi.UpdateSettingsFlow(ctx).Flow(flow.Id).XSessionToken(sessionToken).UpdateSettingsFlowBody( + result, res, err := client.FrontendAPI.UpdateSettingsFlow(ctx).Flow(flow.Id).XSessionToken(sessionToken).UpdateSettingsFlowBody( ory.UpdateSettingsFlowWithPasswordMethodAsUpdateSettingsFlowBody(&ory.UpdateSettingsFlowWithPasswordMethod{ Method: "password", Password: "not-" + password, @@ -51,7 +51,7 @@ func changeTraits(email, password string) *ory.SettingsFlow { sessionToken, flow := initFlow(email, password) // Submit the form - result, res, err := client.FrontendApi.UpdateSettingsFlow(ctx).Flow(flow.Id).XSessionToken(sessionToken).UpdateSettingsFlowBody( + result, res, err := client.FrontendAPI.UpdateSettingsFlow(ctx).Flow(flow.Id).XSessionToken(sessionToken).UpdateSettingsFlowBody( ory.UpdateSettingsFlowWithProfileMethodAsUpdateSettingsFlowBody(&ory.UpdateSettingsFlowWithProfileMethod{ Method: "profile", Traits: map[string]interface{}{ diff --git a/examples/go/selfservice/verification/main.go b/examples/go/selfservice/verification/main.go index cd587f5a8c31..86feb5bfccc9 100644 --- a/examples/go/selfservice/verification/main.go +++ b/examples/go/selfservice/verification/main.go @@ -20,7 +20,7 @@ func performVerification(email string) *ory.VerificationFlow { ctx := context.Background() // Initialize the flow - flow, res, err := client.FrontendApi.CreateNativeVerificationFlow(ctx).Execute() + flow, res, err := client.FrontendAPI.CreateNativeVerificationFlow(ctx).Execute() pkg.SDKExitOnError(err, res) // If you want, print the flow here: @@ -28,7 +28,7 @@ func performVerification(email string) *ory.VerificationFlow { // pkg.PrintJSONPretty(flow) // Submit the form - afterSubmit, res, err := client.FrontendApi.UpdateVerificationFlow(ctx).Flow(flow.Id). + afterSubmit, res, err := client.FrontendAPI.UpdateVerificationFlow(ctx).Flow(flow.Id). UpdateVerificationFlowBody(ory.UpdateVerificationFlowWithLinkMethodAsUpdateVerificationFlowBody(&ory.UpdateVerificationFlowWithLinkMethod{ Email: email, Method: "link", diff --git a/examples/go/session/tosession/main.go b/examples/go/session/tosession/main.go index ec8c30e847b2..05cbd6040bcf 100644 --- a/examples/go/session/tosession/main.go +++ b/examples/go/session/tosession/main.go @@ -19,7 +19,7 @@ func toSession() *ory.Session { email, password := pkg.RandomCredentials() _, sessionToken := pkg.CreateIdentityWithSession(client, email, password) - session, res, err := client.FrontendApi.ToSessionExecute(ory.FrontendApiApiToSessionRequest{}. + session, res, err := client.FrontendAPI.ToSessionExecute(ory.FrontendAPIApiToSessionRequest{}. XSessionToken(sessionToken)) pkg.SDKExitOnError(err, res) return session diff --git a/gen/oidc/v1/state.pb.go b/gen/oidc/v1/state.pb.go new file mode 100644 index 000000000000..ce3ab14d52b1 --- /dev/null +++ b/gen/oidc/v1/state.pb.go @@ -0,0 +1,183 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc (unknown) +// source: oidc/v1/state.proto + +package oidcv1 + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type State struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FlowId []byte `protobuf:"bytes,1,opt,name=flow_id,json=flowId,proto3" json:"flow_id,omitempty"` + SessionTokenExchangeCodeSha512 []byte `protobuf:"bytes,2,opt,name=session_token_exchange_code_sha512,json=sessionTokenExchangeCodeSha512,proto3" json:"session_token_exchange_code_sha512,omitempty"` + ProviderId string `protobuf:"bytes,3,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` + PkceVerifier string `protobuf:"bytes,4,opt,name=pkce_verifier,json=pkceVerifier,proto3" json:"pkce_verifier,omitempty"` +} + +func (x *State) Reset() { + *x = State{} + if protoimpl.UnsafeEnabled { + mi := &file_oidc_v1_state_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *State) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*State) ProtoMessage() {} + +func (x *State) ProtoReflect() protoreflect.Message { + mi := &file_oidc_v1_state_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use State.ProtoReflect.Descriptor instead. +func (*State) Descriptor() ([]byte, []int) { + return file_oidc_v1_state_proto_rawDescGZIP(), []int{0} +} + +func (x *State) GetFlowId() []byte { + if x != nil { + return x.FlowId + } + return nil +} + +func (x *State) GetSessionTokenExchangeCodeSha512() []byte { + if x != nil { + return x.SessionTokenExchangeCodeSha512 + } + return nil +} + +func (x *State) GetProviderId() string { + if x != nil { + return x.ProviderId + } + return "" +} + +func (x *State) GetPkceVerifier() string { + if x != nil { + return x.PkceVerifier + } + return "" +} + +var File_oidc_v1_state_proto protoreflect.FileDescriptor + +var file_oidc_v1_state_proto_rawDesc = []byte{ + 0x0a, 0x13, 0x6f, 0x69, 0x64, 0x63, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x6f, 0x69, 0x64, 0x63, 0x2e, 0x76, 0x31, 0x22, 0xb2, + 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, + 0x64, 0x12, 0x4a, 0x0a, 0x22, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x5f, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, + 0x5f, 0x73, 0x68, 0x61, 0x35, 0x31, 0x32, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x1e, 0x73, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x78, 0x63, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x53, 0x68, 0x61, 0x35, 0x31, 0x32, 0x12, 0x1f, 0x0a, + 0x0b, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, + 0x0a, 0x0d, 0x70, 0x6b, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x6b, 0x63, 0x65, 0x56, 0x65, 0x72, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x42, 0x7c, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x69, 0x64, 0x63, 0x2e, + 0x76, 0x31, 0x42, 0x0a, 0x53, 0x74, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, + 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x72, 0x79, + 0x2f, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x6f, 0x69, 0x64, 0x63, 0x2f, 0x76, 0x31, 0x3b, + 0x6f, 0x69, 0x64, 0x63, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x4f, + 0x69, 0x64, 0x63, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x07, 0x4f, 0x69, 0x64, 0x63, 0x5c, 0x56, 0x31, + 0xe2, 0x02, 0x13, 0x4f, 0x69, 0x64, 0x63, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x08, 0x4f, 0x69, 0x64, 0x63, 0x3a, 0x3a, 0x56, + 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_oidc_v1_state_proto_rawDescOnce sync.Once + file_oidc_v1_state_proto_rawDescData = file_oidc_v1_state_proto_rawDesc +) + +func file_oidc_v1_state_proto_rawDescGZIP() []byte { + file_oidc_v1_state_proto_rawDescOnce.Do(func() { + file_oidc_v1_state_proto_rawDescData = protoimpl.X.CompressGZIP(file_oidc_v1_state_proto_rawDescData) + }) + return file_oidc_v1_state_proto_rawDescData +} + +var file_oidc_v1_state_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_oidc_v1_state_proto_goTypes = []any{ + (*State)(nil), // 0: oidc.v1.State +} +var file_oidc_v1_state_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_oidc_v1_state_proto_init() } +func file_oidc_v1_state_proto_init() { + if File_oidc_v1_state_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_oidc_v1_state_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*State); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_oidc_v1_state_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_oidc_v1_state_proto_goTypes, + DependencyIndexes: file_oidc_v1_state_proto_depIdxs, + MessageInfos: file_oidc_v1_state_proto_msgTypes, + }.Build() + File_oidc_v1_state_proto = out.File + file_oidc_v1_state_proto_rawDesc = nil + file_oidc_v1_state_proto_goTypes = nil + file_oidc_v1_state_proto_depIdxs = nil +} diff --git a/go.mod b/go.mod index 851ee735a919..7fc5a5ac3bba 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,17 @@ module github.com/ory/kratos -go 1.21 +go 1.23 + +toolchain go1.23.2 replace ( - github.com/go-sql-driver/mysql => github.com/go-sql-driver/mysql v1.7.2-0.20231005084435-37980127edfb - github.com/gorilla/sessions => github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 + github.com/go-swagger/go-swagger => github.com/aeneasr/go-swagger v0.19.1-0.20241013070044-bccef3a12e26 // See https://github.com/go-swagger/go-swagger/issues/3131 + // github.com/go-swagger/go-swagger => ../../go-swagger/go-swagger + // https://github.com/gobuffalo/pop/pull/833 + github.com/gobuffalo/pop/v6 => github.com/ory/pop/v6 v6.2.0 - github.com/mattn/go-sqlite3 => github.com/mattn/go-sqlite3 v1.14.16 + github.com/gorilla/sessions => github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 + github.com/mattn/go-sqlite3 => github.com/mattn/go-sqlite3 v1.14.22 // Use the internal httpclient which can be generated in this codebase but mark it as the // official SDK, allowing for the Ory CLI to consume Ory Kratos' CLI commands. @@ -17,207 +22,205 @@ replace ( ) require ( - code.dny.dev/ssrf v0.2.0 // indirect + dario.cat/mergo v1.0.0 github.com/Masterminds/sprig/v3 v3.2.3 github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 github.com/avast/retry-go/v3 v3.1.1 github.com/bradleyjkemp/cupaloy/v2 v2.8.0 - github.com/bwmarrin/discordgo v0.23.0 + github.com/bwmarrin/discordgo v0.28.1 github.com/cenkalti/backoff v2.2.1+incompatible - github.com/cortesi/modd v0.0.0-20210323234521-b35eddab86cc - github.com/davecgh/go-spew v1.1.1 - github.com/davidrjonas/semver-cli v0.0.0-20190116233701-ee19a9a0dda6 + github.com/coreos/go-oidc/v3 v3.11.0 + github.com/cortesi/modd v0.8.1 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc + github.com/dghubble/oauth1 v0.7.3 github.com/dgraph-io/ristretto v0.1.1 - github.com/fatih/color v1.13.0 + github.com/fatih/color v1.17.0 github.com/ghodss/yaml v1.0.0 - github.com/go-crypt/crypt v0.2.9 - github.com/go-faker/faker/v4 v4.2.0 - github.com/go-openapi/strfmt v0.21.7 - github.com/go-playground/validator/v10 v10.4.1 - github.com/go-swagger/go-swagger v0.30.5 - github.com/go-webauthn/webauthn v0.8.4 - github.com/gobuffalo/fizz v1.14.4 + github.com/go-crypt/crypt v0.2.25 + github.com/go-faker/faker/v4 v4.4.2 + github.com/go-openapi/strfmt v0.23.0 + github.com/go-playground/validator/v10 v10.22.0 + github.com/go-swagger/go-swagger v0.31.0 + github.com/go-webauthn/webauthn v0.11.2 github.com/gobuffalo/httptest v1.5.2 github.com/gobuffalo/pop/v6 v6.1.2-0.20230318123913-c85387acc9a0 - github.com/gofrs/uuid v4.3.1+incompatible + github.com/gofrs/uuid v4.4.0+incompatible github.com/golang-jwt/jwt/v4 v4.5.0 - github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2 github.com/golang/mock v1.6.0 - github.com/google/go-github/v27 v27.0.1 github.com/google/go-github/v38 v38.1.0 github.com/google/go-jsonnet v0.20.0 - github.com/gorilla/sessions v1.2.1 + github.com/gorilla/sessions v1.3.0 github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69 - github.com/hashicorp/consul/api v1.20.0 - github.com/hashicorp/go-retryablehttp v0.7.2 - github.com/hashicorp/golang-lru v0.5.4 - github.com/imdario/mergo v0.3.13 + github.com/hashicorp/go-retryablehttp v0.7.7 + github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf - github.com/jarcoal/httpmock v1.0.5 - github.com/jmoiron/sqlx v1.3.5 - github.com/jteeuwen/go-bindata v3.0.7+incompatible + github.com/jarcoal/httpmock v1.3.1 + github.com/jmoiron/sqlx v1.4.0 github.com/julienschmidt/httprouter v1.3.0 github.com/knadh/koanf/parsers/json v0.1.0 github.com/laher/mergefs v0.1.2-0.20230223191438-d16611b2f4e7 - github.com/lestrrat-go/jwx v1.2.28 // indirect + github.com/lestrrat-go/jwx/v2 v2.1.1 github.com/luna-duclos/instrumentedsql v1.1.3 github.com/mailhog/MailHog v1.0.1 - github.com/mattn/goveralls v0.0.7 - github.com/mikefarah/yq/v4 v4.19.1 + github.com/mattn/goveralls v0.0.12 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe github.com/ory/analytics-go/v5 v5.0.1 - github.com/ory/client-go v0.2.0-alpha.60 - github.com/ory/dockertest/v3 v3.9.1 + github.com/ory/client-go v1.14.3 + github.com/ory/dockertest/v3 v3.11.0 github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe github.com/ory/graceful v0.1.4-0.20230301144740-e222150c51d0 github.com/ory/herodot v0.10.3-0.20230626083119-d7e5192f0d88 - github.com/ory/hydra-client-go/v2 v2.2.0-rc.3.0.20240202131107-1c7b57df3bb0 + github.com/ory/hydra-client-go/v2 v2.2.1 github.com/ory/jsonschema/v3 v3.0.8 github.com/ory/mail/v3 v3.0.0 github.com/ory/nosurf v1.2.7 - github.com/ory/x v0.0.614 + github.com/ory/x v0.0.660 github.com/peterhellberg/link v1.2.0 - github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 + github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.4.0 github.com/rakutentech/jwk-go v1.1.3 - github.com/rs/cors v1.8.2 - github.com/samber/lo v1.37.0 - github.com/sirupsen/logrus v1.9.0 - github.com/slack-go/slack v0.7.4 - github.com/spf13/cobra v1.7.0 + github.com/rs/cors v1.11.0 + github.com/samber/lo v1.46.0 + github.com/sirupsen/logrus v1.9.3 + github.com/slack-go/slack v0.13.1 + github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 - github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 - github.com/stretchr/testify v1.8.4 - github.com/tidwall/gjson v1.14.3 + github.com/stretchr/testify v1.9.0 + github.com/tidwall/gjson v1.17.3 github.com/tidwall/sjson v1.2.5 github.com/urfave/negroni v1.0.0 - github.com/zmb3/spotify/v2 v2.4.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 - go.opentelemetry.io/otel v1.22.0 - go.opentelemetry.io/otel/sdk v1.21.0 - go.opentelemetry.io/otel/trace v1.22.0 - golang.org/x/crypto v0.18.0 - golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa - golang.org/x/net v0.20.0 - golang.org/x/oauth2 v0.16.0 - golang.org/x/sync v0.5.0 - golang.org/x/text v0.14.0 - golang.org/x/tools/cmd/cover v0.1.0-deprecated - google.golang.org/grpc v1.59.0 + github.com/zmb3/spotify/v2 v2.4.2 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 + go.opentelemetry.io/otel v1.28.0 + go.opentelemetry.io/otel/sdk v1.28.0 + go.opentelemetry.io/otel/trace v1.28.0 + golang.org/x/crypto v0.26.0 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 + golang.org/x/net v0.27.0 + golang.org/x/oauth2 v0.21.0 + golang.org/x/sync v0.8.0 + golang.org/x/text v0.17.0 + google.golang.org/grpc v1.65.0 ) +require github.com/wI2L/jsondiff v0.6.0 + require ( - github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect + github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect + github.com/bmatcuk/doublestar v1.3.4 // indirect + github.com/cortesi/moddwatch v0.1.0 // indirect + github.com/cortesi/termlog v0.0.0-20210222042314-a1eec763abec // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/rjeczalik/notify v0.9.3 // indirect + golang.org/x/term v0.23.0 // indirect + gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect + mvdan.cc/sh/v3 v3.6.0 // indirect +) + +require ( + code.dny.dev/ssrf v0.2.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver v1.5.0 // indirect - github.com/Masterminds/semver/v3 v3.2.0 // indirect - github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect - github.com/a8m/envsubst v1.3.0 // indirect - github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect - github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect - github.com/armon/go-metrics v0.4.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/avast/retry-go/v4 v4.3.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bmatcuk/doublestar v1.3.4 // indirect github.com/boombuler/barcode v1.0.1 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cockroachdb/cockroach-go/v2 v2.3.5 - github.com/containerd/continuity v0.3.0 // indirect - github.com/cortesi/moddwatch v0.0.0-20210222043437-a6aaad86a36e // indirect - github.com/cortesi/termlog v0.0.0-20210222042314-a1eec763abec // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect - github.com/docker/cli v20.10.21+incompatible // indirect - github.com/docker/distribution v2.8.2+incompatible // indirect - github.com/docker/docker v20.10.24+incompatible // indirect - github.com/docker/go-connections v0.4.0 // indirect + github.com/containerd/continuity v0.4.3 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/cli v26.1.4+incompatible // indirect + github.com/docker/docker v27.1.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect - github.com/elliotchance/orderedmap v1.4.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/felixge/fgprof v0.9.3 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/fxamacker/cbor/v2 v2.4.0 // indirect - github.com/go-crypt/x v0.2.1 // indirect - github.com/go-jose/go-jose/v3 v3.0.1 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-crypt/x v0.2.18 // indirect + github.com/go-jose/go-jose/v3 v3.0.3 // indirect + github.com/go-jose/go-jose/v4 v4.0.2 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/analysis v0.21.4 // indirect - github.com/go-openapi/errors v0.20.4 // indirect - github.com/go-openapi/inflect v0.19.0 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/loads v0.21.2 // indirect - github.com/go-openapi/runtime v0.26.0 // indirect - github.com/go-openapi/spec v0.20.9 // indirect - github.com/go-openapi/swag v0.22.4 // indirect - github.com/go-openapi/validate v0.22.1 // indirect - github.com/go-playground/locales v0.13.0 // indirect - github.com/go-playground/universal-translator v0.17.0 // indirect - github.com/go-sql-driver/mysql v1.7.0 // indirect - github.com/go-webauthn/x v0.1.4 // indirect + github.com/go-openapi/analysis v0.23.0 // indirect + github.com/go-openapi/errors v0.22.0 // indirect + github.com/go-openapi/inflect v0.21.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/loads v0.22.0 // indirect + github.com/go-openapi/runtime v0.28.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/validate v0.24.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/go-webauthn/x v0.1.14 // indirect github.com/gobuffalo/envy v1.10.2 // indirect - github.com/gobuffalo/flect v1.0.0 // indirect - github.com/gobuffalo/github_flavored_markdown v1.1.3 // indirect + github.com/gobuffalo/fizz v1.14.4 // indirect + github.com/gobuffalo/flect v1.0.2 // indirect + github.com/gobuffalo/github_flavored_markdown v1.1.4 // indirect github.com/gobuffalo/helpers v0.6.7 // indirect github.com/gobuffalo/nulls v0.4.2 // indirect - github.com/gobuffalo/plush/v4 v4.1.18 // indirect + github.com/gobuffalo/plush/v4 v4.1.21 // indirect github.com/gobuffalo/tags/v3 v3.1.4 // indirect github.com/gobuffalo/validate/v3 v3.3.3 // indirect github.com/gobwas/glob v0.2.3 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/goccy/go-yaml v1.9.6 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/goccy/go-yaml v1.11.3 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/glog v1.1.2 // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/google/btree v1.0.1 // indirect + github.com/golang/glog v1.2.1 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-querystring v1.0.0 // indirect - github.com/google/go-tpm v0.9.0 // indirect + github.com/google/go-tpm v0.9.1 // indirect github.com/google/pprof v0.0.0-20221010195024-131d412537ea // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/google/uuid v1.3.1 // indirect - github.com/gorilla/context v1.1.1 // indirect - github.com/gorilla/css v1.0.0 // indirect - github.com/gorilla/handlers v1.5.1 // indirect - github.com/gorilla/mux v1.7.3 // indirect - github.com/gorilla/pat v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/context v1.1.2 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/gorilla/handlers v1.5.2 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/pat v1.0.2 // indirect github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-hclog v1.2.0 // indirect - github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/hashicorp/serf v0.10.1 // indirect - github.com/huandu/xstrings v1.3.3 // indirect + github.com/huandu/xstrings v1.4.0 // indirect github.com/ian-kent/envconf v0.0.0-20141026121121-c19809918c02 // indirect github.com/ian-kent/go-log v0.0.0-20160113211217-5731446c36ab // indirect github.com/ian-kent/goose v0.0.0-20141221090059-c3541ea826ad // indirect github.com/ian-kent/linkio v0.0.0-20170807205755-97566b872887 // indirect + github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgconn v1.13.0 // indirect + github.com/jackc/pgconn v1.14.3 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.3.1 // indirect - github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect - github.com/jackc/pgtype v1.12.0 // indirect - github.com/jackc/pgx/v4 v4.17.2 // indirect + github.com/jackc/pgproto3/v2 v2.3.3 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jandelgado/gcov2lcov v1.0.5 // indirect github.com/jessevdk/go-flags v1.5.0 // indirect - github.com/jinzhu/copier v0.3.5 // indirect - github.com/joho/godotenv v1.4.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect @@ -227,13 +230,15 @@ require ( github.com/knadh/koanf/v2 v2.0.1 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect - github.com/leodido/go-urn v1.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc v1.0.6 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/jwx v1.2.29 // indirect github.com/lestrrat-go/option v1.0.1 // indirect - github.com/lib/pq v1.10.7 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailhog/MailHog-Server v1.0.1 // indirect github.com/mailhog/MailHog-UI v1.0.1 // indirect @@ -244,59 +249,59 @@ require ( github.com/mailhog/storage v1.0.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/microcosm-cc/bluemonday v1.0.21 // indirect + github.com/microcosm-cc/bluemonday v1.0.26 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect - github.com/nyaruka/phonenumbers v1.1.6 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/nyaruka/phonenumbers v1.3.6 github.com/ogier/pflag v0.0.1 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0-rc2 // indirect - github.com/opencontainers/runc v1.1.12 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/runc v1.1.14 // indirect github.com/openzipkin/zipkin-go v0.4.2 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/philhofer/fwd v1.1.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect github.com/pkg/profile v1.7.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.13.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect - github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 // indirect + github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/backo-go v1.0.1 // indirect - github.com/sergi/go-diff v1.2.0 // indirect + github.com/sergi/go-diff v1.3.1 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/smartystreets/assertions v1.0.0 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect + github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect - github.com/spf13/afero v1.9.5 // indirect - github.com/spf13/cast v1.5.1 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/viper v1.16.0 // indirect - github.com/subosito/gotenv v1.4.2 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/viper v1.18.2 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/t-k/fluent-logger-golang v1.0.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect - github.com/timtadh/data-structures v0.5.3 // indirect - github.com/timtadh/lexmachine v0.2.2 // indirect - github.com/tinylib/msgp v1.1.8 // indirect + github.com/tinylib/msgp v1.2.0 // indirect github.com/toqueteos/webbrowser v1.2.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect - go.mongodb.org/mongo-driver v1.11.3 // indirect + go.mongodb.org/mongo-driver v1.14.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.47.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.21.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.21.1 // indirect @@ -305,37 +310,20 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect; / indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 // indirect; / indirect go.opentelemetry.io/otel/exporters/zipkin v1.21.0 // indirect; / indirect - go.opentelemetry.io/otel/metric v1.22.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.16.0 // indirect - golang.org/x/tools v0.15.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/mod v0.19.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/tools v0.23.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect - google.golang.org/protobuf v1.31.0 // indirect - gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/protobuf v1.34.2 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect - gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - mvdan.cc/sh/v3 v3.3.0-0.dev.0.20210224101809-fb5052e7a010 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) - -require ( - github.com/coreos/go-oidc/v3 v3.9.0 - github.com/lestrrat-go/jwx/v2 v2.0.19 -) - -require ( - github.com/jackc/puddle/v2 v2.1.2 // indirect - github.com/lestrrat-go/httprc v1.0.4 // indirect - github.com/segmentio/asm v1.2.0 // indirect - go.uber.org/atomic v1.10.0 // indirect -) diff --git a/go.sum b/go.sum index 8be4e2b20487..b87e9f92c9af 100644 --- a/go.sum +++ b/go.sum @@ -3,7 +3,6 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= @@ -14,9 +13,6 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -34,26 +30,27 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= code.dny.dev/ssrf v0.2.0 h1:wCBP990rQQ1CYfRpW+YK1+8xhwUjv189AQ3WMo1jQaI= code.dny.dev/ssrf v0.2.0/go.mod h1:B+91l25OnyaLIeCx0WRJN5qfJ/4/ZTZxRXgm0lj/2w8= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= -github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= -github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= @@ -62,23 +59,18 @@ github.com/Wikia/x v0.0.614-0.20240308102603-87531a56005d h1:uEKBK3y0kAL4ptGom7A github.com/Wikia/x v0.0.614-0.20240308102603-87531a56005d/go.mod h1:uH065puz8neija0neqwIN3PmXXfDsB9VbZTZ20Znoos= github.com/a8m/envsubst v1.3.0 h1:GmXKmVssap0YtlU3E230W98RWtWCyIZzjtf1apWWyAg= github.com/a8m/envsubst v1.3.0/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY= +github.com/aeneasr/go-swagger v0.19.1-0.20241013070044-bccef3a12e26 h1:rwCKVbnpzxQ0F/AhO9FkXnrKqRmqej4epjhe1CpNkB0= +github.com/aeneasr/go-swagger v0.19.1-0.20241013070044-bccef3a12e26/go.mod h1:WSigRRWEig8zV6t6Sm8Y+EmUjlzA/HoaZJ5edupq7po= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4= -github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-metrics v0.4.0 h1:yCQqn7dwca4ITXb+CbubHmedzaQYHhNhrEXLYUeEe8Q= -github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/avast/retry-go/v3 v3.1.1 h1:49Scxf4v8PmiQ/nY0aY3p0hDueqSmc7++cBbtiDGu2g= @@ -91,7 +83,6 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= @@ -101,113 +92,107 @@ github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyX github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= -github.com/bwmarrin/discordgo v0.23.0 h1://ARp8qUrRZvDGMkfAjtcC20WOvsMtTgi+KrdKnl6eY= -github.com/bwmarrin/discordgo v0.23.0/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M= +github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= +github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/cockroach-go/v2 v2.3.5 h1:Khtm8K6fTTz/ZCWPzU9Ne3aOW9VyAnj4qIPCJgKtwK0= github.com/cockroachdb/cockroach-go/v2 v2.3.5/go.mod h1:1wNJ45eSXW9AnOc3skntW9ZUZz6gxrQK3cOj3rK+BC8= -github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= -github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= -github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= -github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/cortesi/modd v0.0.0-20210323234521-b35eddab86cc h1:JPkUs3TdeYIbBy9GTDdez9rBbbl6E9BNRa9jlfEDjnE= -github.com/cortesi/modd v0.0.0-20210323234521-b35eddab86cc/go.mod h1:h20oNPW+cbtizAJwm90svrR7HlV7XvSNT5di+GP7SbA= -github.com/cortesi/moddwatch v0.0.0-20210222043437-a6aaad86a36e h1:vNbhR09qtq9ELJgvhAWng4zl/4CVTPBPVev3R8MlUYc= -github.com/cortesi/moddwatch v0.0.0-20210222043437-a6aaad86a36e/go.mod h1:MUkYRZrwFTHATqCI5tDJRPqmBt9xf3q4+Avfut7kCCE= +github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= +github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= +github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= +github.com/cortesi/modd v0.8.1 h1:0s8e10CJ6pxc6NQHYFrmUZOLP0X6v63ry+3na6Gq2Ow= +github.com/cortesi/modd v0.8.1/go.mod h1:GDJFkhHnnW+SD1C+wHBlKe5Yh2IqiOb6Lu5t2/fjnS4= +github.com/cortesi/moddwatch v0.1.0 h1:+TSMuplhKlKEPKsdUXNHd67aCqew+et15dJvRCxMd1M= +github.com/cortesi/moddwatch v0.1.0/go.mod h1:PFkhcmmwsRMQ76IMjKbaIIMcGQt7BSMtFOp+pA0B2eo= github.com/cortesi/termlog v0.0.0-20210222042314-a1eec763abec h1:v7D8uHsIKsyjfyhhNdY4qivqN558Ejiq+CDXiUljZ+4= github.com/cortesi/termlog v0.0.0-20210222042314-a1eec763abec/go.mod h1:10Fm2kasJmcKf1FSMQGSWb976sfR29hejNtfS9AydB4= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davidrjonas/semver-cli v0.0.0-20190116233701-ee19a9a0dda6 h1:VzPvKOw28XJ77PYwOq5gAqvFB4gk6gst0HxxiW8kfZQ= -github.com/davidrjonas/semver-cli v0.0.0-20190116233701-ee19a9a0dda6/go.mod h1:+6FzxsSbK4oEuvdN06Jco8zKB2mQqIB6UduZdd0Zesk= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/dghubble/oauth1 v0.7.3 h1:EkEM/zMDMp3zOsX2DC/ZQ2vnEX3ELK0/l9kb+vs4ptE= +github.com/dghubble/oauth1 v0.7.3/go.mod h1:oxTe+az9NSMIucDPDCCtzJGsPhciJV33xocHfcR2sVY= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/docker/cli v20.10.21+incompatible h1:qVkgyYUnOLQ98LtXBrwd/duVqPT2X4SHndOuGsfwyhU= -github.com/docker/cli v20.10.21+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE= -github.com/docker/docker v20.10.24+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8= +github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/elliotchance/orderedmap v1.4.0 h1:wZtfeEONCbx6in1CZyE6bELEt/vFayMvsxqI5SgsR+A= -github.com/elliotchance/orderedmap v1.4.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= -github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= -github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-crypt/crypt v0.2.9 h1:5gWWTId2Qyqs9ROIsegt5pnqo9wUSRLbhpkR6JSftjg= -github.com/go-crypt/crypt v0.2.9/go.mod h1:JjzdTYE2mArb6nBoIvvpF7o46/rK/1pfmlArCRMTFUk= -github.com/go-crypt/x v0.2.1 h1:OGw78Bswme9lffCOX6tyuC280ouU5391glsvThMtM5U= -github.com/go-crypt/x v0.2.1/go.mod h1:Q/y9rms7yw4/1CavBlNGn0Itg4HqwNpe1N9FX0TxXrc= -github.com/go-faker/faker/v4 v4.2.0 h1:dGebOupKwssrODV51E0zbMrv5e2gO9VWSLNC1WDCpWg= -github.com/go-faker/faker/v4 v4.2.0/go.mod h1:F/bBy8GH9NxOxMInug5Gx4WYeG6fHJZ8Ol/dhcpRub4= +github.com/go-crypt/crypt v0.2.25 h1:uW/3n4/9zYSOOgY0Md9dMxGSqrjaMyLo1/IFrm2L5yw= +github.com/go-crypt/crypt v0.2.25/go.mod h1:ny8BOunn+/kr99iq2LYSKA0MAsxNaxZUmKKL42vV1io= +github.com/go-crypt/x v0.2.18 h1:KdUGj4D/PdzcIkOQhK36QHzH2YD5GWrsVQ7JgO73Q8Y= +github.com/go-crypt/x v0.2.18/go.mod h1:z48dMLdgMGPPjrzni9ETtg2foPez3EytjCL43Ak4QZ8= +github.com/go-faker/faker/v4 v4.4.2 h1:96WeU9QKEqRUVYdjHquY2/5bAqmVM0IfGKHV5mbfqmQ= +github.com/go-faker/faker/v4 v4.4.2/go.mod h1:4K3v4AbKXYNHMQNaREMc9/kRB9j5JJzpFo6KHRvrcIw= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= -github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= +github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -217,149 +202,99 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= -github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= -github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= -github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= -github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= -github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= -github.com/go-openapi/errors v0.20.4 h1:unTcVm6PispJsMECE3zWgvG4xTiKda1LIR5rCRWLG6M= -github.com/go-openapi/errors v0.20.4/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= -github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= -github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g= -github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro= -github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= -github.com/go-openapi/runtime v0.26.0 h1:HYOFtG00FM1UvqrcxbEJg/SwvDRvYLQKGhw2zaQjTcc= -github.com/go-openapi/runtime v0.26.0/go.mod h1:QgRGeZwrUcSHdeh4Ka9Glvo0ug1LC5WyE+EV88plZrQ= -github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= -github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= -github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= -github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= -github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg= -github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= -github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= -github.com/go-openapi/strfmt v0.21.7 h1:rspiXgNWgeUzhjo1YU01do6qsahtJNByjLVbPLNHb8k= -github.com/go-openapi/strfmt v0.21.7/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= -github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/validate v0.22.1 h1:G+c2ub6q47kfX1sOBLwIQwzBVt8qmOAARyo/9Fqs9NU= -github.com/go-openapi/validate v0.22.1/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= -github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/go-sql-driver/mysql v1.7.2-0.20231005084435-37980127edfb h1:R5sscofA9Cyd0h2DNMbR8BHYVKj1ZTOgUah1PoU4OVU= -github.com/go-sql-driver/mysql v1.7.2-0.20231005084435-37980127edfb/go.mod h1:6gYm/zDt3ahdnMVTPeT/LfoBFsws1qZm5yI6FmVjB14= +github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= +github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= +github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= +github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= +github.com/go-openapi/inflect v0.21.0 h1:FoBjBTQEcbg2cJUWX6uwL9OyIW8eqc9k4KhN4lfbeYk= +github.com/go-openapi/inflect v0.21.0/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= +github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= +github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= +github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= +github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= +github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-swagger/go-swagger v0.30.5 h1:SQ2+xSonWjjoEMOV5tcOnZJVlfyUfCBhGQGArS1b9+U= -github.com/go-swagger/go-swagger v0.30.5/go.mod h1:cWUhSyCNqV7J1wkkxfr5QmbcnCewetCdvEXqgPvbc/Q= -github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013 h1:l9rI6sNaZgNC0LnF3MiE+qTmyBA/tZAg1rtyrGbUMK0= -github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.mod h1:b65mBPzqzZWxOZGxSWrqs4GInLIn+u99Q9q7p+GKni0= github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-webauthn/webauthn v0.8.4 h1:/emQ9b9Rj4flWO94Fo8KJeYvZ6VzPywXsmqyDA/WicY= -github.com/go-webauthn/webauthn v0.8.4/go.mod h1:ZqEa9OnSCdQf6CJvTWTDCsUcPRi8F3h7XCIDINwbBgI= -github.com/go-webauthn/x v0.1.4 h1:sGmIFhcY70l6k7JIDfnjVBiAAFEssga5lXIUXe0GtAs= -github.com/go-webauthn/x v0.1.4/go.mod h1:75Ug0oK6KYpANh5hDOanfDI+dvPWHk788naJVG/37H8= -github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= -github.com/gobuffalo/attrs v1.0.3/go.mod h1:KvDJCE0avbufqS0Bw3UV7RQynESY0jjod+572ctX4t8= -github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= -github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= -github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= -github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc= +github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0= +github.com/go-webauthn/x v0.1.14 h1:1wrB8jzXAofojJPAaRxnZhRgagvLGnLjhCAwg3kTpT0= +github.com/go-webauthn/x v0.1.14/go.mod h1:UuVvFZ8/NbOnkDz3y1NaxtUN87pmtpC1PQ+/5BBQRdc= github.com/gobuffalo/envy v1.10.2 h1:EIi03p9c3yeuRCFPOKcSfajzkLb3hrRjEpHGI8I2Wo4= github.com/gobuffalo/envy v1.10.2/go.mod h1:qGAGwdvDsaEtPhfBzb3o0SfDea8ByGn9j8bKmVft9z8= github.com/gobuffalo/fizz v1.14.4 h1:8uume7joF6niTNWN582IQ2jhGTUoa9g1fiV/tIoGdBs= github.com/gobuffalo/fizz v1.14.4/go.mod h1:9/2fGNXNeIFOXEEgTPJwiK63e44RjG+Nc4hfMm1ArGM= -github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= -github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= -github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= github.com/gobuffalo/flect v0.3.0/go.mod h1:5pf3aGnsvqvCj50AVni7mJJF8ICxGZ8HomberC3pXLE= -github.com/gobuffalo/flect v1.0.0 h1:eBFmskjXZgAOagiTXJH25Nt5sdFwNRcb8DKZsIsAUQI= -github.com/gobuffalo/flect v1.0.0/go.mod h1:l9V6xSb4BlXwsxEMj3FVEub2nkdQjWhPvD8XTTlHPQc= -github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= -github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= -github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= -github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= -github.com/gobuffalo/genny/v2 v2.1.0/go.mod h1:4yoTNk4bYuP3BMM6uQKYPvtP6WsXFGm2w2EFYZdRls8= -github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= -github.com/gobuffalo/github_flavored_markdown v1.1.3 h1:rSMPtx9ePkFB22vJ+dH+m/EUBS8doQ3S8LeEXcdwZHk= +github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= +github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gobuffalo/github_flavored_markdown v1.1.3/go.mod h1:IzgO5xS6hqkDmUh91BW/+Qxo/qYnvfzoz3A7uLkg77I= -github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= -github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= -github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= +github.com/gobuffalo/github_flavored_markdown v1.1.4 h1:WacrEGPXUDX+BpU1GM/Y0ADgMzESKNWls9hOTG1MHVs= +github.com/gobuffalo/github_flavored_markdown v1.1.4/go.mod h1:Vl9686qrVVQou4GrHRK/KOG3jCZOKLUqV8MMOAYtlso= github.com/gobuffalo/helpers v0.6.7 h1:C9CedoRSfgWg2ZoIkVXgjI5kgmSpL34Z3qdnzpfNVd8= github.com/gobuffalo/helpers v0.6.7/go.mod h1:j0u1iC1VqlCaJEEVkZN8Ia3TEzfj/zoXANqyJExTMTA= github.com/gobuffalo/here v0.6.7 h1:hpfhh+kt2y9JLDfhYUxxCRxQol540jsVfKUZzjlbp8o= github.com/gobuffalo/here v0.6.7/go.mod h1:vuCfanjqckTuRlqAitJz6QC4ABNnS27wLb816UhsPcc= github.com/gobuffalo/httptest v1.5.2 h1:GpGy520SfY1QEmyPvaqmznTpG4gEQqQ82HtHqyNEreM= github.com/gobuffalo/httptest v1.5.2/go.mod h1:FA23yjsWLGj92mVV74Qtc8eqluc11VqcWr8/C1vxt4g= -github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= -github.com/gobuffalo/logger v1.0.7/go.mod h1:u40u6Bq3VVvaMcy5sRBclD8SXhBYPS0Qk95ubt+1xJM= -github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= -github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= github.com/gobuffalo/nulls v0.4.2 h1:GAqBR29R3oPY+WCC7JL9KKk9erchaNuV6unsOSZGQkw= github.com/gobuffalo/nulls v0.4.2/go.mod h1:EElw2zmBYafU2R9W4Ii1ByIj177wA/pc0JdjtD0EsH8= -github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= -github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= -github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7IAyxFT8= -github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= -github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= github.com/gobuffalo/plush/v4 v4.1.16/go.mod h1:6t7swVsarJ8qSLw1qyAH/KbrcSTwdun2ASEQkOznakg= -github.com/gobuffalo/plush/v4 v4.1.18 h1:bnPjdMTEUQHqj9TNX2Ck3mxEXYZa+0nrFMNM07kpX9g= -github.com/gobuffalo/plush/v4 v4.1.18/go.mod h1:xi2tJIhFI4UdzIL8sxZtzGYOd2xbBpcFbLZlIPGGZhU= -github.com/gobuffalo/pop/v6 v6.1.2-0.20230318123913-c85387acc9a0 h1:+LF3Enal3HZ+rFmaLZfBRNHKqtnoA0d8jk0Iio8InZM= -github.com/gobuffalo/pop/v6 v6.1.2-0.20230318123913-c85387acc9a0/go.mod h1:1n7jAmI1i7fxuXPZjZb0VBPQDbksRtCoFnrDV5IsvaI= -github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= +github.com/gobuffalo/plush/v4 v4.1.21 h1:YVfauGshxyQ+beh4jHR6Ct3NEXohn+1EboMjzdUDo30= +github.com/gobuffalo/plush/v4 v4.1.21/go.mod h1:WiKHJx3qBvfaDVlrv8zT7NCd3dEMaVR/fVxW4wqV17M= github.com/gobuffalo/tags/v3 v3.1.4 h1:X/ydLLPhgXV4h04Hp2xlbI2oc5MDaa7eub6zw8oHjsM= github.com/gobuffalo/tags/v3 v3.1.4/go.mod h1:ArRNo3ErlHO8BtdA0REaZxijuWnWzF6PUXngmMXd2I0= github.com/gobuffalo/validate/v3 v3.3.3 h1:o7wkIGSvZBYBd6ChQoLxkz2y1pfmhbI4jNJYh6PuNJ4= github.com/gobuffalo/validate/v3 v3.3.3/go.mod h1:YC7FsbJ/9hW/VjQdmXPvFqvRis4vrRYFxr69WiNZw6g= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-yaml v1.9.6 h1:KhAu1zf9JXnm3vbG49aDE0E5uEBUsM4uwD31/58ZWyI= -github.com/goccy/go-yaml v1.9.6/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I= +github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= -github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= -github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2 h1:xisWqjiKEff2B0KfFYGpCqc3M3zdTz+OHQHRc09FeYk= github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= -github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= +github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= +github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -388,13 +323,10 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -402,26 +334,24 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-github/v27 v27.0.1 h1:sSMFSShNn4VnqCqs+qhab6TS3uQc+uVR6TD1bW6MavM= -github.com/google/go-github/v27 v27.0.1/go.mod h1:/0Gr8pJ55COkmv+S/yPKCczSkUPIM/LnFyubufRNIS0= github.com/google/go-github/v38 v38.1.0 h1:C6h1FkaITcBFK7gAmq4eFzt6gbhEhk7L5z6R3Uva+po= github.com/google/go-github/v38 v38.1.0/go.mod h1:cStvrz/7nFr0FoENgG6GLbp53WaelXucT+BBz/3VKx4= github.com/google/go-jsonnet v0.20.0 h1:WG4TTSARuV7bSm4PMB4ohjxe33IHT5WVTrJSU33uT4g= github.com/google/go-jsonnet v0.20.0/go.mod h1:VbgWF9JX7ztlv770x/TolZNGGFfiHEVx9G6ca2eUmeA= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= -github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= +github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM= +github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -429,37 +359,33 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/pprof v0.0.0-20221010195024-131d412537ea h1:R3VfsTXMMK4JCWZDdxScmnTzu9n9YRsDvguLis0U/b8= github.com/google/pprof v0.0.0-20221010195024-131d412537ea/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= +github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= -github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= -github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/pat v1.0.1 h1:OeSoj6sffw4/majibAY2BAUsXjNP7fEE+w30KickaL4= -github.com/gorilla/pat v1.0.1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/pat v1.0.2 h1:TDh/RulbnPxMQACcwbgMF5Bf00jaGoeYBNu+XUFuwtE= +github.com/gorilla/pat v1.0.2/go.mod h1:ioQ7dFQ2KXmOmWLJs6vZAfRikcm2D2JyuLrL9b5wVCg= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -469,59 +395,23 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 h1:6UKoz5ujsI55KNpsJH3UwCq3T8k github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y= github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69 h1:7xsUJsB2NrdcttQPa7JLEaGzvdbk7KvfrjgHZXOQRo0= github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69/go.mod h1:YLEMZOtU+AZ7dhN9T/IpGhXVGly2bvkJQ+zxj3WeVQo= -github.com/hashicorp/consul/api v1.20.0 h1:9IHTjNVSZ7MIwjlW3N3a7iGiykCMDpxZu8jsxFJh0yc= -github.com/hashicorp/consul/api v1.20.0/go.mod h1:nR64eD44KQ59Of/ECwt2vUmIK2DKsDzAwTmwmLl8Wpo= -github.com/hashicorp/consul/sdk v0.13.1 h1:EygWVWWMczTzXGpO93awkHFzfUka6hLYJ0qhETd+6lY= -github.com/hashicorp/consul/sdk v0.13.1/go.mod h1:SW/mM4LbKfqmMvcFu8v+eiQQ7oitXEFeiBe9StxERb0= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= -github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= -github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= -github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= -github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= -github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= -github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= -github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= -github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= -github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= -github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ian-kent/envconf v0.0.0-20141026121121-c19809918c02 h1:dU8zq210pt1b71X8xh9GOxC7uBHNtQ9BYC+Lb6SA/mA= github.com/ian-kent/envconf v0.0.0-20141026121121-c19809918c02/go.mod h1:1m5fo3aKG2moYtGHC4I2nFkXmG97+vCeaEIWC+mXTSI= github.com/ian-kent/go-log v0.0.0-20160113211217-5731446c36ab h1:OgrFrYWlVzY7Tc8rq7Y4ErlKo28igc70gbfJGTVWTJk= @@ -531,104 +421,68 @@ github.com/ian-kent/goose v0.0.0-20141221090059-c3541ea826ad/go.mod h1:VHyJj0/IJ github.com/ian-kent/linkio v0.0.0-20170807205755-97566b872887 h1:LPaZmcRJS13h+igi07S26uKy0qxCa76u1+pArD+JGrY= github.com/ian-kent/linkio v0.0.0-20170807205755-97566b872887/go.mod h1:aE63iKqF9rMrshaEiYZroUYFZLaYoTuA7pBMsg3lJoY= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= -github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= +github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= -github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= -github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E= -github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle/v2 v2.1.2 h1:0f7vaaXINONKTsxYDn4otOAiJanX/BMeAtY//BXqzlg= -github.com/jackc/puddle/v2 v2.1.2/go.mod h1:2lpufsF5mRHO6SuZkm0fNYxM6SWHfvyFj62KwNzgels= +github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= +github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= +github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jandelgado/gcov2lcov v1.0.5 h1:rkBt40h0CVK4oCb8Dps950gvfd1rYvQ8+cWa346lVU0= github.com/jandelgado/gcov2lcov v1.0.5/go.mod h1:NnSxK6TMlg1oGDBfGelGbjgorT5/L3cchlbtgFYZSss= -github.com/jarcoal/httpmock v1.0.5 h1:cHtVEcTxRSX4J0je7mWPfc9BpDpqzXSJ5HbymZmyHck= -github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= +github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= +github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= -github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= -github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= -github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jteeuwen/go-bindata v3.0.7+incompatible h1:91Uy4d9SYVr1kyTJ15wJsog+esAZZl7JmEfTkwmhJts= -github.com/jteeuwen/go-bindata v3.0.7+incompatible/go.mod h1:JVvhzYOiGBnFSYRyV00iY8q7/0PThjIYav1p9h5dmKs= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= -github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/parsers/json v0.1.0 h1:dzSZl5pf5bBcW0Acnu20Djleto19T0CfHcvZ14NJ6fU= @@ -644,9 +498,7 @@ github.com/knadh/koanf/providers/rawbytes v0.1.0/go.mod h1:mMTB1/IcJ/yE++A2iEZbY github.com/knadh/koanf/v2 v2.0.1 h1:1dYGITt1I23x8cfx8ZnldtezdyaZtfAuRtIFOiRzK7g= github.com/knadh/koanf/v2 v2.0.1/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -654,37 +506,33 @@ github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NB github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/laher/mergefs v0.1.2-0.20230223191438-d16611b2f4e7 h1:PDeBswTUsSIT4QSrzLvlqKlGrANYa7TrXUwdBN9myU8= github.com/laher/mergefs v0.1.2-0.20230223191438-d16611b2f4e7/go.mod h1:FSY1hYy94on4Tz60waRMGdO1awwS23BacqJlqf9lJ9Q= -github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= -github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= +github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx v1.2.28 h1:uadI6o0WpOVrBSf498tRXZIwPpEtLnR9CvqPFXeI5sA= -github.com/lestrrat-go/jwx v1.2.28/go.mod h1:nF+91HEMh/MYFVwKPl5HHsBGMPscqbQb+8IDQdIazP8= -github.com/lestrrat-go/jwx/v2 v2.0.19 h1:ekv1qEZE6BVct89QA+pRF6+4pCpfVrOnEJnTnT4RXoY= -github.com/lestrrat-go/jwx/v2 v2.0.19/go.mod h1:l3im3coce1lL2cDeAjqmaR+Awx+X8Ih+2k8BuHNJ4CU= +github.com/lestrrat-go/jwx v1.2.29 h1:QT0utmUJ4/12rmsVQrJ3u55bycPkKqGYuGT4tyRhxSQ= +github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtXSNFeZuB8= +github.com/lestrrat-go/jwx/v2 v2.1.1 h1:Y2ltVl8J6izLYFs54BVcpXLv5msSW4o8eXwnzZLI32E= +github.com/lestrrat-go/jwx/v2 v2.1.1/go.mod h1:4LvZg7oxu6Q5VJwn7Mk/UwooNRnTHUpXBj2C4j3HNx0= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= -github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/luna-duclos/instrumentedsql v1.1.3 h1:t7mvC0z1jUt5A0UQ6I/0H31ryymuQRnJcWCiqV3lSAA= github.com/luna-duclos/instrumentedsql v1.1.3/go.mod h1:9J1njvFds+zN7y85EDhN9XNQLANWwZt2ULeIC8yMNYs= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -705,66 +553,47 @@ github.com/mailhog/smtp v1.0.1 h1:igL3N/L+pWuGCqUaje21HX3VIVnqHoVlqWO0t+wJEYE= github.com/mailhog/smtp v1.0.1/go.mod h1:GMrAdv1hXro38xj5dsWPAk5ZiXJHFx9t7W9Yqsk0XUM= github.com/mailhog/storage v1.0.1 h1:uut2nlG5hIxbsl6f8DGznPAHwQLf3/7Na2t4gmrIais= github.com/mailhog/storage v1.0.1/go.mod h1:4EAUf5xaEVd7c/OhvSxOOwQ66jT6q2er+BDBQ0EVrew= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno= github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= -github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= -github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/mattn/goveralls v0.0.7 h1:vzy0i4a2iDzEFMdXIxcanRadkr0FBvSBKUmj0P8SPlQ= -github.com/mattn/goveralls v0.0.7/go.mod h1:h8b4ow6FxSPMQHF6o2ve3qsclnffZjYTNEKmLesRwqw= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/goveralls v0.0.12 h1:PEEeF0k1SsTjOBQ8FOmrOAoCu4ytuMaWCnWe94zxbCg= +github.com/mattn/goveralls v0.0.12/go.mod h1:44ImGEUfmqH8bBtaMrYKsM65LXfNLWmwaxFGjZwgMSQ= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= +github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50= -github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= -github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= -github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= -github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= -github.com/mikefarah/yq/v4 v4.19.1 h1:QrZCqjOBZ918aOZIfl/IwnHBv104SPfarhgO5MGd2W4= -github.com/mikefarah/yq/v4 v4.19.1/go.mod h1:krTElh9V1fv3Cw7+21S8El/W/vn3f2buOOcJ4VyjsFY= -github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/microcosm-cc/bluemonday v1.0.22/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= +github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI= -github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -778,9 +607,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nyaruka/phonenumbers v1.1.6 h1:DcueYq7QrOArAprAYNoQfDgp0KetO4LqtnBtQC6Wyes= -github.com/nyaruka/phonenumbers v1.1.6/go.mod h1:yShPJHDSH3aTKzCbXyVxNpbl2kA+F+Ne5Pun/MvFRos= +github.com/nyaruka/phonenumbers v1.3.6 h1:33owXWp4d1U+Tyaj9fpci6PbvaQZcXBUO2FybeKeLwQ= +github.com/nyaruka/phonenumbers v1.3.6/go.mod h1:Ut+eFwikULbmCenH6InMKL9csUNLyxHuBLyfkpum11s= github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750= github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= @@ -794,24 +622,24 @@ github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= -github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= -github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss= -github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w= +github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA= github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY= github.com/ory/analytics-go/v5 v5.0.1 h1:LX8T5B9FN8KZXOtxgN+R3I4THRRVB6+28IKgKBpXmAM= github.com/ory/analytics-go/v5 v5.0.1/go.mod h1:lWCiCjAaJkKfgR/BN5DCLMol8BjKS1x+4jxBxff/FF0= -github.com/ory/dockertest/v3 v3.9.1 h1:v4dkG+dlu76goxMiTT2j8zV7s4oPPEppKT8K8p2f1kY= -github.com/ory/dockertest/v3 v3.9.1/go.mod h1:42Ir9hmvaAPm0Mgibk6mBPi7SFvTXxEcnztDYOJ//uM= +github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= +github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe h1:rvu4obdvqR0fkSIJ8IfgzKOWwZ5kOT2UNfLq81Qk7rc= github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe/go.mod h1:z4n3u6as84LbV4YmgjHhnwtccQqzf4cZlSk9f1FhygI= github.com/ory/graceful v0.1.4-0.20230301144740-e222150c51d0 h1:VMUeLRfQD14fOMvhpYZIIT4vtAqxYh+f3KnSqCeJ13o= github.com/ory/graceful v0.1.4-0.20230301144740-e222150c51d0/go.mod h1:hg2iCy+LCWOXahBZ+NQa4dk8J2govyQD79rrqrgMyY8= github.com/ory/herodot v0.10.3-0.20230626083119-d7e5192f0d88 h1:J0CIFKdpUeqKbVMw7pQ1qLtUnflRM1JWAcOEq7Hp4yg= github.com/ory/herodot v0.10.3-0.20230626083119-d7e5192f0d88/go.mod h1:MMNmY6MG1uB6fnXYFaHoqdV23DTWctlPsmRCeq/2+wc= -github.com/ory/hydra-client-go/v2 v2.2.0-rc.3.0.20240202131107-1c7b57df3bb0 h1:D5w0EQBqZU5UcdW0iLTxqbBiEAhOwT2cWHWE6vjxJ3o= -github.com/ory/hydra-client-go/v2 v2.2.0-rc.3.0.20240202131107-1c7b57df3bb0/go.mod h1:JwnnsLd402LPTmIA+EDMsu5Nwr6IRl777pE0QvOq66c= +github.com/ory/hydra-client-go/v2 v2.2.1 h1:m1821pIX6ybG/3oSAn2wtrbBKNwe9q5A8fLljYuLpBk= +github.com/ory/hydra-client-go/v2 v2.2.1/go.mod h1:K83R+iK40+5uF2uQ34yRUrf9izRvFsza9pG2Se5qMmk= github.com/ory/jsonschema/v3 v3.0.8 h1:Ssdb3eJ4lDZ/+XnGkvQS/te0p+EkolqwTsDOCxr/FmU= github.com/ory/jsonschema/v3 v3.0.8/go.mod h1:ZPzqjDkwd3QTnb2Z6PAS+OTvBE2x5i6m25wCGx54W/0= github.com/ory/mail v2.3.1+incompatible/go.mod h1:87D9/1gB6ewElQoN0lXJ0ayfqcj3cW3qCTXh+5E9mfU= @@ -819,26 +647,22 @@ github.com/ory/mail/v3 v3.0.0 h1:8LFMRj473vGahFD/ntiotWEd4S80FKYFtiZTDfOQ+sM= github.com/ory/mail/v3 v3.0.0/go.mod h1:JGAVeZF8YAlxbaFDUHqRZAKBCSeW2w1vuxf28hFbZAw= github.com/ory/nosurf v1.2.7 h1:YrHrbSensQyU6r6HT/V5+HPdVEgrOTMJiLoJABSBOp4= github.com/ory/nosurf v1.2.7/go.mod h1:d4L3ZBa7Amv55bqxCBtCs63wSlyaiCkWVl4vKf3OUxA= +github.com/ory/pop/v6 v6.2.0 h1:hRFOGAOEHw91kUHQ32k5NHqCkcHrRou/romvrJP1w0E= +github.com/ory/pop/v6 v6.2.0/go.mod h1:okVAYKGtgunD/wbW3NGhZTndJCS+6FqO+cA89rQ4doc= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 h1:zm6sDvHy/U9XrGpixwHiuAwpp0Ock6khSVHkrv6lQQU= github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/ory/x v0.0.614 h1:amqUBxoY5Z0fN+WqH1sLLtGuJa5GYOBo76LyrwJC0dc= -github.com/ory/x v0.0.614/go.mod h1:uH065puz8neija0neqwIN3PmXXfDsB9VbZTZ20Znoos= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/ory/x v0.0.660 h1:mEZjmVtPY5grN3bmuSPkJBTK7xSNepzy0bCmVOCLZxU= +github.com/ory/x v0.0.660/go.mod h1:tS0FyZXpVeKd1lCcFgV/Rb1GlccI/Xq8DraFS+lmIt8= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c= github.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc= -github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= -github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= -github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= -github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= -github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 h1:jYi87L8j62qkXzaYHAQAhEapgukhenIMZRBKTNRLHJ4= +github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -846,16 +670,13 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= @@ -868,7 +689,6 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= @@ -876,7 +696,6 @@ github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8 github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= @@ -884,28 +703,22 @@ github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5 github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/rakutentech/jwk-go v1.1.3 h1:PiLwepKyUaW+QFG3ki78DIO2+b4IVK3nMhlxM70zrQ4= github.com/rakutentech/jwk-go v1.1.3/go.mod h1:LtzSv4/+Iti1nnNeVQiP6l5cI74GBStbhyXCYvgPZFk= -github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1 h1:FLWDC+iIP9BWgYKvWKKtOUZux35LIQNAuIzp/63RQJU= -github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM= -github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= +github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= -github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw= -github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/samber/lo v1.46.0 h1:w8G+oaCPgz1PoCJztqymCFaKwXt+5cCXn51uPxExFfQ= +github.com/samber/lo v1.46.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 h1:0b8DF5kR0PhRoRXDiEEdzrgBc8UqVY4JWLkQJCRsLME= github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761/go.mod h1:/THDZYi7F/BsVEcYzYPqdcWFQ+1C2InkawTKfLOAnzg= github.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48= @@ -917,22 +730,19 @@ github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/ github.com/segmentio/conf v1.2.0/go.mod h1:Y3B9O/PqqWqjyxyWWseyj/quPEtMu1zDp/kVbSWWaB0= github.com/segmentio/go-snakecase v1.1.0/go.mod h1:jk1miR5MS7Na32PZUykG89Arm+1BUSYhuGR6b7+hJto= github.com/segmentio/objconv v1.0.1/go.mod h1:auayaH5k3137Cl4SoXTgrzQcuQDmvuVtZgS0fb1Ahys= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/slack-go/slack v0.7.4 h1:Z+7CmUDV+ym4lYLA4NNLFIpr3+nDgViHrx8xsuXgrYs= -github.com/slack-go/slack v0.7.4/go.mod h1:FGqNzJBmxIsZURAxh2a8D21AnOVvvXZvGligs4npPUM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/slack-go/slack v0.13.1 h1:6UkM3U1OnbhPsYeb1IMkQ6HSNOSikWluwOncJt4Tz/o= +github.com/slack-go/slack v0.13.1/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= @@ -940,32 +750,27 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= -github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= -github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= -github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= -github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 h1:iD+PFTQwKEmbwSdwfvP5ld2WEI/g7qbdhmHJ2ASfYGs= -github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -975,42 +780,33 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= -github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/t-k/fluent-logger-golang v1.0.0 h1:4IQzY+/l66Zkkhk9eB3LwF9vPkgKHJ1rpYdrRiap0EI= github.com/t-k/fluent-logger-golang v1.0.0/go.mod h1:6vC3Vzp9Kva0l5J9+YDY5/ROePwkAqwLK+KneCjSm4w= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= -github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94= +github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/timtadh/data-structures v0.5.3 h1:F2tEjoG9qWIyUjbvXVgJqEOGJPMIiYn7U5W5mE+i/vQ= -github.com/timtadh/data-structures v0.5.3/go.mod h1:9R4XODhJ8JdWFEI8P/HJKqxuJctfBQw6fDibMQny2oU= -github.com/timtadh/lexmachine v0.2.2 h1:g55RnjdYazm5wnKv59pwFcBJHOyvTPfDEoz21s4PHmY= -github.com/timtadh/lexmachine v0.2.2/go.mod h1:GBJvD5OAfRn/gnp92zb9KTgHLB7akKyxmVivoYCcjQI= -github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= -github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/tinylib/msgp v1.2.0 h1:0uKB/662twsVBpYUPbokj4sTSKhWFKB7LopO2kWK8lY= +github.com/tinylib/msgp v1.2.0/go.mod h1:2vIGs3lcUo8izAATNobrCHevYZC/LMsJtw4JPiYPHro= github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ= github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/wI2L/jsondiff v0.6.0 h1:zrsH3FbfVa3JO9llxrcDy/XLkYPLgoMX6Mz3T2PP2AI= +github.com/wI2L/jsondiff v0.6.0/go.mod h1:D6aQ5gKgPF9g17j+E9N7aasmU1O+XvfmWm1y8UMmNpw= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= -github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= -github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= -github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -1020,39 +816,33 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -github.com/zmb3/spotify/v2 v2.4.0 h1:ZHdhBx/Qyn7rtVDP+onk/oSvtL5uVyJtb+VBLrNDC7Y= -github.com/zmb3/spotify/v2 v2.4.0/go.mod h1:m6c3mHgZSt1rTF76UfSfdn1Gb2Kx/B/ClCcr+2V1Scw= -go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= -go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= -go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= -go.mongodb.org/mongo-driver v1.11.3 h1:Ql6K6qYHEzB6xvu4+AU0BoRoqf9vFPcc4o7MUIdPW8Y= -go.mongodb.org/mongo-driver v1.11.3/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= +github.com/zmb3/spotify/v2 v2.4.2 h1:j3yNN5lKVEMZQItJF4MHCSZbfNWmXO+KaC+3RFaLlLc= +github.com/zmb3/spotify/v2 v2.4.2/go.mod h1:XOV7BrThayFYB9AAfB+L0Q0wyxBuLCARk4fI/ZXCBW8= +go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= +go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.47.0 h1:rw+yB4sMhufNzbVHGG9SDMSrw1CKSnRqfjJnMpAH4dE= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.47.0/go.mod h1:2NonlJyJNVbDK/hCwiLsu5gsD2bVtmIzQ/tGzWq58us= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= go.opentelemetry.io/contrib/propagators/b3 v1.21.0 h1:uGdgDPNzwQWRwCXJgw/7h29JaRqcq9B87Iv4hJDKAZw= go.opentelemetry.io/contrib/propagators/b3 v1.21.0/go.mod h1:D9GQXvVGT2pzyTfp1QBOnD1rzKEWzKjjwu5q2mslCUI= go.opentelemetry.io/contrib/propagators/jaeger v1.21.1 h1:f4beMGDKiVzg9IcX7/VuWVy+oGdjx3dNJ72YehmtY5k= go.opentelemetry.io/contrib/propagators/jaeger v1.21.1/go.mod h1:U9jhkEl8d1LL+QXY7q3kneJWJugiN3kZJV2OWz3hkBY= go.opentelemetry.io/contrib/samplers/jaegerremote v0.15.1 h1:Qb+5A+JbIjXwO7l4HkRUhgIn4Bzz0GNS2q+qdmSx+0c= go.opentelemetry.io/contrib/samplers/jaegerremote v0.15.1/go.mod h1:G4vNCm7fRk0kjZ6pGNLo5SpLxAUvOfSrcaegnT8TPck= -go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= -go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= @@ -1061,56 +851,34 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkE go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= go.opentelemetry.io/otel/exporters/zipkin v1.21.0 h1:D+Gv6lSfrFBWmQYyxKjDd0Zuld9SRXpIrEsKZvE4DO4= go.opentelemetry.io/otel/exporters/zipkin v1.21.0/go.mod h1:83oMKR6DzmHisFOW3I+yIMGZUTjxiWaiBI8M8+TU5zE= -go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= -go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= -go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= -go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= -go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1121,8 +889,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1135,7 +903,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -1144,14 +911,12 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1167,8 +932,6 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1184,17 +947,10 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210323141857-08027d57d8cf/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= -golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -1202,30 +958,29 @@ golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfS golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= -golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1236,32 +991,24 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1283,23 +1030,13 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201029080932-201ba4db2418/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1308,56 +1045,54 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY= -golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -1365,25 +1100,15 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1391,7 +1116,6 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -1406,30 +1130,18 @@ golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWc golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= -golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= -golang.org/x/tools/cmd/cover v0.1.0-deprecated h1:Rwy+mWYz6loAF+LnG1jHG/JWMHRMMC2/1XX3Ejkx9lA= -golang.org/x/tools/cmd/cover v0.1.0-deprecated/go.mod h1:hMDiIvlpN1NoVgmjLjUJE9tMHyxHjFX7RuQ+rW12mSA= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1452,9 +1164,6 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1462,8 +1171,6 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1493,19 +1200,10 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= -google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= -google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= -google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= +google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= +google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1518,12 +1216,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= -google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/grpc/examples v0.0.0-20210304020650-930c79186c99 h1:qA8rMbz1wQ4DOFfM2ouD29DG9aHWBm6ZOy9BGxiUMmY= google.golang.org/grpc/examples v0.0.0-20210304020650-930c79186c99/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -1538,9 +1232,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= @@ -1548,7 +1242,6 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= @@ -1556,14 +1249,11 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/mold.v2 v2.2.0/go.mod h1:XMyyRsGtakkDPbxXbrA5VODo6bUXyvoDjLd5l3T0XoA= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= -gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE= -gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= @@ -1571,20 +1261,14 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= -gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1592,9 +1276,9 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -mvdan.cc/editorconfig v0.1.1-0.20200121172147-e40951bde157/go.mod h1:Ge4atmRUYqueGppvJ7JNrtqpqokoJEFxYbP0Z+WeKS8= -mvdan.cc/sh/v3 v3.3.0-0.dev.0.20210224101809-fb5052e7a010 h1:0xJA1YM0Ppa63jEfcdPsjRHo1qxklwXWhIPr9tAQ2J4= -mvdan.cc/sh/v3 v3.3.0-0.dev.0.20210224101809-fb5052e7a010/go.mod h1:fPQmabBpREM/XQ9YXSU5ZFZ/Sm+PmKP9/vkFHgYKJEI= +mvdan.cc/editorconfig v0.2.0/go.mod h1:lvnnD3BNdBYkhq+B4uBuFFKatfp02eB6HixDvEz91C0= +mvdan.cc/sh/v3 v3.6.0 h1:gtva4EXJ0dFNvl5bHjcUEvws+KRcDslT8VKheTYkbGU= +mvdan.cc/sh/v3 v3.6.0/go.mod h1:U4mhtBLZ32iWhif5/lD+ygy1zrgaQhUu+XFy7C8+TTA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/go_mod_indirect_pins.go b/go_mod_indirect_pins.go index c01015abe0a2..15d9aecaf52f 100644 --- a/go_mod_indirect_pins.go +++ b/go_mod_indirect_pins.go @@ -7,22 +7,10 @@ package main import ( + _ "github.com/cortesi/modd/cmd/modd" _ "github.com/go-swagger/go-swagger/cmd/swagger" + _ "github.com/mailhog/MailHog" _ "github.com/mattn/goveralls" - _ "github.com/sqs/goreturns" - _ "golang.org/x/tools/cmd/cover" - - _ "github.com/gobuffalo/fizz" _ "github.com/ory/go-acc" - - _ "github.com/jteeuwen/go-bindata" - - _ "github.com/davidrjonas/semver-cli" - - _ "github.com/cortesi/modd/cmd/modd" - _ "github.com/hashicorp/consul/api" - - _ "github.com/mailhog/MailHog" - _ "github.com/mikefarah/yq/v4" ) diff --git a/hash/hash_comparator.go b/hash/hash_comparator.go index b28ebf0484b1..9a70931fa012 100644 --- a/hash/hash_comparator.go +++ b/hash/hash_comparator.go @@ -9,8 +9,8 @@ import ( "crypto/aes" "crypto/cipher" "crypto/hmac" - "crypto/md5" //#nosec G501 -- compatibility for imported passwords - "crypto/sha1" //#nosec G505 -- compatibility for imported passwords + "crypto/md5" //nolint:all // System compatibility for imported passwords + "crypto/sha1" //nolint:all // System compatibility for imported passwords "crypto/sha256" "crypto/sha512" "crypto/subtle" @@ -21,23 +21,19 @@ import ( "regexp" "strings" + "github.com/go-crypt/crypt" + "github.com/go-crypt/crypt/algorithm/md5crypt" + "github.com/go-crypt/crypt/algorithm/shacrypt" "github.com/gofrs/uuid" "github.com/pkg/errors" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "golang.org/x/crypto/argon2" "golang.org/x/crypto/bcrypt" - - //nolint:staticcheck - //lint:ignore SA1019 compatibility for imported passwords - "golang.org/x/crypto/md4" //#nosec G501 -- compatibility for imported passwords + "golang.org/x/crypto/md4" //nolint:all // System compatibility for imported passwords "golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/scrypt" - "github.com/go-crypt/crypt" - "github.com/go-crypt/crypt/algorithm/md5crypt" - "github.com/go-crypt/crypt/algorithm/shacrypt" - "github.com/ory/kratos/driver/config" ) diff --git a/hydra/hydra.go b/hydra/hydra.go index a4466307ec9d..3332fb120157 100644 --- a/hydra/hydra.go +++ b/hydra/hydra.go @@ -72,7 +72,7 @@ func (h *DefaultHydra) getAdminURL(ctx context.Context) (string, error) { return u.String(), nil } -func (h *DefaultHydra) getAdminAPIClient(ctx context.Context) (*hydraclientgo.OAuth2ApiService, error) { +func (h *DefaultHydra) getAdminAPIClient(ctx context.Context) (hydraclientgo.OAuth2API, error) { url, err := h.getAdminURL(ctx) if err != nil { return nil, err @@ -87,7 +87,7 @@ func (h *DefaultHydra) getAdminAPIClient(ctx context.Context) (*hydraclientgo.OA } configuration.HTTPClient = client - return hydraclientgo.NewAPIClient(configuration).OAuth2Api, nil + return hydraclientgo.NewAPIClient(configuration).OAuth2API, nil } func (h *DefaultHydra) AcceptLoginRequest(ctx context.Context, params AcceptLoginRequestParams) (string, error) { diff --git a/hydra/hydra_test.go b/hydra/hydra_test.go index b2e252be5b18..d022ae6021cf 100644 --- a/hydra/hydra_test.go +++ b/hydra/hydra_test.go @@ -13,6 +13,7 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/hydra" "github.com/ory/x/configx" + "github.com/ory/x/contextx" "github.com/ory/x/logrusx" "github.com/ory/x/sqlxx" "github.com/ory/x/urlx" @@ -25,11 +26,12 @@ func requestFromChallenge(s string) *http.Request { func TestGetLoginChallengeID(t *testing.T) { uuidChallenge := "b346a452-e8fb-4828-8ef8-a4dbc98dc23a" blobChallenge := "1337deadbeefcafe" - defaultConfig := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) + defaultConfig := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation()) configWithHydra := config.MustNew( t, logrusx.New("", ""), os.Stderr, + &contextx.Default{}, configx.SkipValidation(), configx.WithValues(map[string]interface{}{ config.ViperKeyOAuth2ProviderURL: "https://hydra", diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_password_migration_hook_enabled.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_password_migration_hook_enabled.json new file mode 100644 index 000000000000..e89fc60f2d8b --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_password_migration_hook_enabled.json @@ -0,0 +1,22 @@ +{ + "credentials": { + "password": { + "type": "password", + "identifiers": [ + "pw-migration-hook@ory.sh" + ], + "config": { + "use_password_migration_hook": true + }, + "version": 0 + } + }, + "schema_id": "default", + "state": "active", + "traits": { + "email": "pw-migration-hook@ory.sh" + }, + "metadata_public": null, + "metadata_admin": null, + "organization_id": null +} diff --git a/identity/.snapshots/TestHandler-case=should_list_all_identities_with_credentials-include_credential=oidc_should_include_OIDC_credentials_config.json b/identity/.snapshots/TestHandler-case=should_list_all_identities_with_credentials-include_credential=oidc_should_include_OIDC_credentials_config.json new file mode 100644 index 000000000000..95bff506986a --- /dev/null +++ b/identity/.snapshots/TestHandler-case=should_list_all_identities_with_credentials-include_credential=oidc_should_include_OIDC_credentials_config.json @@ -0,0 +1 @@ +"{\"providers\":[{\"initial_id_token\":\"id_token0\",\"initial_access_token\":\"access_token0\",\"initial_refresh_token\":\"refresh_token0\",\"subject\":\"foo\",\"provider\":\"bar\",\"organization\":\"\"},{\"initial_id_token\":\"id_token1\",\"initial_access_token\":\"access_token1\",\"initial_refresh_token\":\"refresh_token1\",\"subject\":\"baz\",\"provider\":\"zab\",\"organization\":\"\"}]}" diff --git a/identity/.snapshots/TestSchemaExtensionCredentials-case=0.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=0.json new file mode 100644 index 000000000000..9d4512a908be --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=0.json @@ -0,0 +1,6 @@ +{ + "type": "password", + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestSchemaExtensionCredentials-case=1.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=1.json new file mode 100644 index 000000000000..9d4512a908be --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=1.json @@ -0,0 +1,6 @@ +{ + "type": "password", + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestSchemaExtensionCredentials-case=10.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=10.json new file mode 100644 index 000000000000..b189c3022df0 --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=10.json @@ -0,0 +1,18 @@ +{ + "type": "code", + "config": { + "addresses": [ + { + "channel": "sms", + "address": "+4917667111638" + }, + { + "channel": "email", + "address": "foo@ory.sh" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestSchemaExtensionCredentials-case=11.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=11.json new file mode 100644 index 000000000000..b189c3022df0 --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=11.json @@ -0,0 +1,18 @@ +{ + "type": "code", + "config": { + "addresses": [ + { + "channel": "sms", + "address": "+4917667111638" + }, + { + "channel": "email", + "address": "foo@ory.sh" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestSchemaExtensionCredentials-case=12.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=12.json new file mode 100644 index 000000000000..968d3fa35bdd --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=12.json @@ -0,0 +1,22 @@ +{ + "type": "code", + "config": { + "addresses": [ + { + "channel": "sms", + "address": "+4917667111638" + }, + { + "channel": "email", + "address": "bar@ory.sh" + }, + { + "channel": "email", + "address": "foo@ory.sh" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestSchemaExtensionCredentials-case=13.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=13.json new file mode 100644 index 000000000000..0a7e7320cbaa --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=13.json @@ -0,0 +1,14 @@ +{ + "type": "code", + "config": { + "addresses": [ + { + "channel": "email", + "address": "bar@ory.sh" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestSchemaExtensionCredentials-case=2.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=2.json new file mode 100644 index 000000000000..5a19603d9a00 --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=2.json @@ -0,0 +1,6 @@ +{ + "type": "webauthn", + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestSchemaExtensionCredentials-case=3.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=3.json new file mode 100644 index 000000000000..9d4512a908be --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=3.json @@ -0,0 +1,6 @@ +{ + "type": "password", + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestSchemaExtensionCredentials-case=4.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=4.json new file mode 100644 index 000000000000..5a19603d9a00 --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=4.json @@ -0,0 +1,6 @@ +{ + "type": "webauthn", + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestSchemaExtensionCredentials-case=5.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=5.json new file mode 100644 index 000000000000..5a19603d9a00 --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=5.json @@ -0,0 +1,6 @@ +{ + "type": "webauthn", + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestSchemaExtensionCredentials-case=6.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=6.json new file mode 100644 index 000000000000..2663d5b1297a --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=6.json @@ -0,0 +1,14 @@ +{ + "type": "code", + "config": { + "addresses": [ + { + "channel": "email", + "address": "foo@ory.sh" + } + ] + }, + "version": 1, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestSchemaExtensionCredentials-case=7.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=7.json new file mode 100644 index 000000000000..6b8c682b5077 --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=7.json @@ -0,0 +1,14 @@ +{ + "type": "code", + "config": { + "addresses": [ + { + "channel": "email", + "address": "foo@ory.sh" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestSchemaExtensionCredentials-case=8.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=8.json new file mode 100644 index 000000000000..6b8c682b5077 --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=8.json @@ -0,0 +1,14 @@ +{ + "type": "code", + "config": { + "addresses": [ + { + "channel": "email", + "address": "foo@ory.sh" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestSchemaExtensionCredentials-case=9.json b/identity/.snapshots/TestSchemaExtensionCredentials-case=9.json new file mode 100644 index 000000000000..b189c3022df0 --- /dev/null +++ b/identity/.snapshots/TestSchemaExtensionCredentials-case=9.json @@ -0,0 +1,18 @@ +{ + "type": "code", + "config": { + "addresses": [ + { + "channel": "sms", + "address": "+4917667111638" + }, + { + "channel": "email", + "address": "foo@ory.sh" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestUpgradeCredentials-type=code-from=v0_with_correct_value.json b/identity/.snapshots/TestUpgradeCredentials-type=code-from=v0_with_correct_value.json new file mode 100644 index 000000000000..3673c6943f03 --- /dev/null +++ b/identity/.snapshots/TestUpgradeCredentials-type=code-from=v0_with_correct_value.json @@ -0,0 +1,30 @@ +{ + "id": "4d64fa08-20fc-450d-bebd-ebd7c7b6e249", + "credentials": { + "code": { + "type": "code", + "identifiers": [ + "hi@example.org" + ], + "config": { + "addresses": [ + { + "channel": "email", + "address": "hi@example.org" + } + ] + }, + "version": 1, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + }, + "schema_id": "", + "schema_url": "", + "state": "", + "traits": null, + "metadata_public": null, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "organization_id": null +} diff --git a/identity/.snapshots/TestUpgradeCredentials-type=code-from=v0_with_email_empty_space_value-with_one_identifier.json b/identity/.snapshots/TestUpgradeCredentials-type=code-from=v0_with_email_empty_space_value-with_one_identifier.json new file mode 100644 index 000000000000..3673c6943f03 --- /dev/null +++ b/identity/.snapshots/TestUpgradeCredentials-type=code-from=v0_with_email_empty_space_value-with_one_identifier.json @@ -0,0 +1,30 @@ +{ + "id": "4d64fa08-20fc-450d-bebd-ebd7c7b6e249", + "credentials": { + "code": { + "type": "code", + "identifiers": [ + "hi@example.org" + ], + "config": { + "addresses": [ + { + "channel": "email", + "address": "hi@example.org" + } + ] + }, + "version": 1, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + }, + "schema_id": "", + "schema_url": "", + "state": "", + "traits": null, + "metadata_public": null, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "organization_id": null +} diff --git a/identity/.snapshots/TestUpgradeCredentials-type=code-from=v0_with_email_empty_space_value-with_two_identifiers.json b/identity/.snapshots/TestUpgradeCredentials-type=code-from=v0_with_email_empty_space_value-with_two_identifiers.json new file mode 100644 index 000000000000..9163f3c3886b --- /dev/null +++ b/identity/.snapshots/TestUpgradeCredentials-type=code-from=v0_with_email_empty_space_value-with_two_identifiers.json @@ -0,0 +1,35 @@ +{ + "id": "4d64fa08-20fc-450d-bebd-ebd7c7b6e249", + "credentials": { + "code": { + "type": "code", + "identifiers": [ + "foo@example.org", + "bar@example.org" + ], + "config": { + "addresses": [ + { + "channel": "email", + "address": "foo@example.org" + }, + { + "channel": "email", + "address": "bar@example.org" + } + ] + }, + "version": 1, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + }, + "schema_id": "", + "schema_url": "", + "state": "", + "traits": null, + "metadata_public": null, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "organization_id": null +} diff --git a/identity/.snapshots/TestUpgradeCredentials-type=code-from=v0_with_empty_value.json b/identity/.snapshots/TestUpgradeCredentials-type=code-from=v0_with_empty_value.json new file mode 100644 index 000000000000..3673c6943f03 --- /dev/null +++ b/identity/.snapshots/TestUpgradeCredentials-type=code-from=v0_with_empty_value.json @@ -0,0 +1,30 @@ +{ + "id": "4d64fa08-20fc-450d-bebd-ebd7c7b6e249", + "credentials": { + "code": { + "type": "code", + "identifiers": [ + "hi@example.org" + ], + "config": { + "addresses": [ + { + "channel": "email", + "address": "hi@example.org" + } + ] + }, + "version": 1, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + }, + "schema_id": "", + "schema_url": "", + "state": "", + "traits": null, + "metadata_public": null, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "organization_id": null +} diff --git a/identity/.snapshots/TestUpgradeCredentials-type=code-from=v0_with_unknown_value.json b/identity/.snapshots/TestUpgradeCredentials-type=code-from=v0_with_unknown_value.json new file mode 100644 index 000000000000..3673c6943f03 --- /dev/null +++ b/identity/.snapshots/TestUpgradeCredentials-type=code-from=v0_with_unknown_value.json @@ -0,0 +1,30 @@ +{ + "id": "4d64fa08-20fc-450d-bebd-ebd7c7b6e249", + "credentials": { + "code": { + "type": "code", + "identifiers": [ + "hi@example.org" + ], + "config": { + "addresses": [ + { + "channel": "email", + "address": "hi@example.org" + } + ] + }, + "version": 1, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + }, + "schema_id": "", + "schema_url": "", + "state": "", + "traits": null, + "metadata_public": null, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "organization_id": null +} diff --git a/identity/.snapshots/TestUpgradeCredentials-type=code-from=v2_with_empty_value.json b/identity/.snapshots/TestUpgradeCredentials-type=code-from=v2_with_empty_value.json new file mode 100644 index 000000000000..a52b01ffd526 --- /dev/null +++ b/identity/.snapshots/TestUpgradeCredentials-type=code-from=v2_with_empty_value.json @@ -0,0 +1,35 @@ +{ + "id": "4d64fa08-20fc-450d-bebd-ebd7c7b6e249", + "credentials": { + "code": { + "type": "code", + "identifiers": [ + "foo@example.org", + "+12341234" + ], + "config": { + "addresses": [ + { + "address": "foo@example.org", + "channel": "email" + }, + { + "address": "+12341234", + "channel": "sms" + } + ] + }, + "version": 1, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } + }, + "schema_id": "", + "schema_url": "", + "state": "", + "traits": null, + "metadata_public": null, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "organization_id": null +} diff --git a/identity/.snapshots/TestUpgradeCredentials-type=webauthn-from=v1.json b/identity/.snapshots/TestUpgradeCredentials-type=webauthn-from=v1.json index 60038539418a..4931ed3f972d 100644 --- a/identity/.snapshots/TestUpgradeCredentials-type=webauthn-from=v1.json +++ b/identity/.snapshots/TestUpgradeCredentials-type=webauthn-from=v1.json @@ -3,7 +3,7 @@ "credentials": { "webauthn": { "type": "webauthn", - "identifiers": null, + "identifiers": [], "config": { "credentials": [ { diff --git a/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=oidc.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=oidc.json new file mode 100644 index 000000000000..a967e155d02a --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=oidc.json @@ -0,0 +1,22 @@ +{ + "type": "oidc", + "identifiers": [ + "bar", + "baz" + ], + "config": { + "providers": [ + { + "initial_id_token": "foo", + "initial_access_token": "", + "initial_refresh_token": "", + "subject": "", + "provider": "", + "organization": "" + } + ] + }, + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=password.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=password.json new file mode 100644 index 000000000000..1939a8fe4f71 --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=password.json @@ -0,0 +1,10 @@ +{ + "type": "password", + "identifiers": [ + "zab", + "bar" + ], + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=webauthn.json b/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=webauthn.json new file mode 100644 index 000000000000..1b7dcd8f6204 --- /dev/null +++ b/identity/.snapshots/TestWithDeclassifiedCredentials-case=oidc-credential=webauthn.json @@ -0,0 +1,10 @@ +{ + "type": "webauthn", + "identifiers": [ + "foo", + "bar" + ], + "version": 0, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" +} diff --git a/identity/address.go b/identity/address.go index 2e9175642e84..ae7ab83e38e7 100644 --- a/identity/address.go +++ b/identity/address.go @@ -5,4 +5,5 @@ package identity const ( AddressTypeEmail = "email" + AddressTypeSMS = "sms" ) diff --git a/identity/credentials.go b/identity/credentials.go index c18b9df97f5d..9fc2d93851bb 100644 --- a/identity/credentials.go +++ b/identity/credentials.go @@ -10,6 +10,7 @@ import ( "time" "github.com/gofrs/uuid" + "github.com/wI2L/jsondiff" "github.com/ory/kratos/ui/node" "github.com/ory/x/sqlxx" @@ -86,6 +87,8 @@ const ( CredentialsTypeLookup CredentialsType = "lookup_secret" CredentialsTypeWebAuthn CredentialsType = "webauthn" CredentialsTypeCodeAuth CredentialsType = "code" + CredentialsTypePasskey CredentialsType = "passkey" + CredentialsTypeProfile CredentialsType = "profile" ) func (c CredentialsType) String() string { @@ -106,6 +109,8 @@ func (c CredentialsType) ToUiNodeGroup() node.UiNodeGroup { return node.LookupGroup case CredentialsTypeCodeAuth: return node.CodeGroup + case CredentialsTypePasskey: + return node.PasskeyGroup default: return node.DefaultGroup } @@ -118,6 +123,7 @@ var AllCredentialTypes = []CredentialsType{ CredentialsTypeLookup, CredentialsTypeWebAuthn, CredentialsTypeCodeAuth, + CredentialsTypePasskey, } const ( @@ -138,6 +144,7 @@ func ParseCredentialsType(in string) (CredentialsType, bool) { CredentialsTypeCodeAuth, CredentialsTypeRecoveryLink, CredentialsTypeRecoveryCode, + CredentialsTypePasskey, } { if t.String() == in { return t, true @@ -209,8 +216,8 @@ type ( // swagger:ignore ActiveCredentialsCounter interface { ID() CredentialsType - CountActiveFirstFactorCredentials(cc map[CredentialsType]Credentials) (int, error) - CountActiveMultiFactorCredentials(cc map[CredentialsType]Credentials) (int, error) + CountActiveFirstFactorCredentials(context.Context, map[CredentialsType]Credentials) (int, error) + CountActiveMultiFactorCredentials(context.Context, map[CredentialsType]Credentials) (int, error) } // swagger:ignore @@ -242,7 +249,13 @@ func CredentialsEqual(a, b map[CredentialsType]Credentials) bool { return false } - if string(expect.Config) != string(actual.Config) { + // Try to normalize configs (remove spaces etc). + patch, err := jsondiff.CompareJSON(actual.Config, expect.Config) + if err != nil { + return false + } + + if len(patch) > 0 { return false } diff --git a/identity/credentials_code.go b/identity/credentials_code.go index b7f828ae09a1..e3e5f174f84c 100644 --- a/identity/credentials_code.go +++ b/identity/credentials_code.go @@ -4,22 +4,63 @@ package identity import ( - "database/sql" + "encoding/json" + + "github.com/ory/herodot" + + "github.com/pkg/errors" + + "github.com/ory/x/stringsx" ) -type CodeAddressType = string +type CodeChannel string const ( - CodeAddressTypeEmail CodeAddressType = AddressTypeEmail + CodeChannelEmail CodeChannel = AddressTypeEmail + CodeChannelSMS CodeChannel = AddressTypeSMS ) +func NewCodeChannel(value string) (CodeChannel, error) { + switch f := stringsx.SwitchExact(value); { + case f.AddCase(string(CodeChannelEmail)): + return CodeChannelEmail, nil + case f.AddCase(string(CodeChannelSMS)): + return CodeChannelSMS, nil + default: + return "", errors.Wrap(ErrInvalidCodeAddressType, f.ToUnknownCaseErr().Error()) + } +} + // CredentialsCode represents a one time login/registration code // // swagger:model identityCredentialsCode type CredentialsCode struct { + Addresses []CredentialsCodeAddress `json:"addresses"` +} + +// swagger:model identityCredentialsCodeAddress +type CredentialsCodeAddress struct { // The type of the address for this code - AddressType CodeAddressType `json:"address_type"` + Channel CodeChannel `json:"channel"` + + // The address for this code + Address string `json:"address"` +} + +var ErrInvalidCodeAddressType = herodot.ErrInternalServerError.WithReasonf("The address type for sending OTP codes is not supported.") + +func (c *CredentialsCodeAddress) UnmarshalJSON(data []byte) (err error) { + type alias CredentialsCodeAddress + var ac alias + if err := json.Unmarshal(data, &ac); err != nil { + return err + } + + ac.Channel, err = NewCodeChannel(string(ac.Channel)) + if err != nil { + return err + } - // UsedAt indicates whether and when a recovery code was used. - UsedAt sql.NullTime `json:"used_at,omitempty"` + *c = CredentialsCodeAddress(ac) + return nil } diff --git a/identity/credentials_code_test.go b/identity/credentials_code_test.go new file mode 100644 index 000000000000..475885024faa --- /dev/null +++ b/identity/credentials_code_test.go @@ -0,0 +1,110 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCredentialsCodeAddressUnmarshalJSON(t *testing.T) { + tests := []struct { + name string + input string + want CredentialsCodeAddress + wantErr bool + }{ + { + name: "valid email address", + input: `{"channel": "email", "address": "user@example.com"}`, + want: CredentialsCodeAddress{ + Channel: CodeChannelEmail, + Address: "user@example.com", + }, + wantErr: false, + }, + { + name: "valid SMS address", + input: `{"channel": "sms", "address": "+1234567890"}`, + want: CredentialsCodeAddress{ + Channel: CodeChannelSMS, + Address: "+1234567890", + }, + wantErr: false, + }, + { + name: "invalid address type", + input: `{"channel": "invalid", "address": "user@example.com"}`, + want: CredentialsCodeAddress{}, + wantErr: true, + }, + { + name: "missing channel field", + input: `{"address": "user@example.com"}`, + want: CredentialsCodeAddress{}, + wantErr: true, + }, + { + name: "invalid JSON structure", + input: `{"channel": "email", "address": "user@example.com"`, + want: CredentialsCodeAddress{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got CredentialsCodeAddress + err := json.Unmarshal([]byte(tt.input), &got) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestNewCodeAddressType(t *testing.T) { + tests := []struct { + name string + input string + want CodeChannel + wantErr bool + }{ + { + name: "valid email address type", + input: "email", + want: CodeChannelEmail, + wantErr: false, + }, + { + name: "valid SMS address type", + input: "sms", + want: CodeChannelSMS, + wantErr: false, + }, + { + name: "invalid address type", + input: "invalid", + want: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewCodeChannel(tt.input) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} diff --git a/identity/credentials_migrate.go b/identity/credentials_migrate.go index f856be135a29..dea1a8a44b8d 100644 --- a/identity/credentials_migrate.go +++ b/identity/credentials_migrate.go @@ -6,6 +6,7 @@ package identity import ( "encoding/json" "fmt" + "strings" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -60,7 +61,61 @@ func UpgradeCredentials(i *Identity) error { if err := UpgradeWebAuthnCredentials(i, &c); err != nil { return errors.WithStack(err) } + if err := UpgradeCodeCredentials(&c); err != nil { + return errors.WithStack(err) + } i.Credentials[k] = c } return nil } + +func UpgradeCodeCredentials(c *Credentials) (err error) { + if c.Type != CredentialsTypeCodeAuth { + return nil + } + + version := c.Version + if version == 0 { + addressType := strings.ToLower(strings.TrimSpace(gjson.GetBytes(c.Config, "address_type").String())) + + channel, err := NewCodeChannel(addressType) + if err != nil { + // We know that in some cases the address type can be empty. In this case, we default to email + // as sms is a new addition to the address_type introduced in this PR. + channel = CodeChannelEmail + } + + c.Config, err = sjson.DeleteBytes(c.Config, "used_at") + if err != nil { + return errors.WithStack(err) + } + + c.Config, err = sjson.DeleteBytes(c.Config, "address_type") + if err != nil { + return errors.WithStack(err) + } + + for _, id := range c.Identifiers { + if id == "" { + continue + } + + c.Config, err = sjson.SetBytes(c.Config, "addresses.-1", &CredentialsCodeAddress{ + Address: id, + Channel: channel, + }) + if err != nil { + return errors.WithStack(err) + } + } + + // This is needed because sjson adds spaces which can trip string comparisons. + c.Config, err = json.Marshal(json.RawMessage(c.Config)) + if err != nil { + return errors.WithStack(err) + } + + c.Version = 1 + } + return nil +} diff --git a/identity/credentials_migrate_test.go b/identity/credentials_migrate_test.go index 2916bc892bde..43291d63e771 100644 --- a/identity/credentials_migrate_test.go +++ b/identity/credentials_migrate_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -30,43 +31,63 @@ func TestUpgradeCredentials(t *testing.T) { snapshotx.SnapshotTExcept(t, &wc, nil) }) - identityID := uuid.FromStringOrNil("4d64fa08-20fc-450d-bebd-ebd7c7b6e249") + run := func(t *testing.T, identifiers []string, config string, version int, credentialsType CredentialsType, expectedVersion int) { + if identifiers == nil { + identifiers = []string{"hi@example.org"} + } + i := &Identity{ + ID: uuid.FromStringOrNil("4d64fa08-20fc-450d-bebd-ebd7c7b6e249"), + Credentials: map[CredentialsType]Credentials{ + credentialsType: { + Identifiers: identifiers, + Type: credentialsType, + Version: version, + Config: []byte(config), + }}, + } + + require.NoError(t, UpgradeCredentials(i)) + wc := WithCredentialsAndAdminMetadataInJSON(*i) + snapshotx.SnapshotT(t, &wc) + assert.Equal(t, expectedVersion, i.Credentials[credentialsType].Version) + } + + t.Run("type=code", func(t *testing.T) { + t.Run("from=v0 with email empty space value", func(t *testing.T) { + t.Run("with one identifier", func(t *testing.T) { + run(t, nil, `{"address_type": "email ", "used_at": {"Time": "0001-01-01T00:00:00Z", "Valid": false}}`, 0, CredentialsTypeCodeAuth, 1) + }) + + t.Run("with two identifiers", func(t *testing.T) { + run(t, []string{"foo@example.org", "bar@example.org"}, `{"address_type": "email ", "used_at": {"Time": "0001-01-01T00:00:00Z", "Valid": false}}`, 0, CredentialsTypeCodeAuth, 1) + }) + }) + + t.Run("from=v0 with empty value", func(t *testing.T) { + run(t, nil, `{"address_type": "", "used_at": {"Time": "0001-01-01T00:00:00Z", "Valid": false}}`, 0, CredentialsTypeCodeAuth, 1) + }) + + t.Run("from=v0 with correct value", func(t *testing.T) { + run(t, nil, `{"address_type": "email", "used_at": {"Time": "0001-01-01T00:00:00Z", "Valid": false}}`, 0, CredentialsTypeCodeAuth, 1) + }) + + t.Run("from=v0 with unknown value", func(t *testing.T) { + run(t, nil, `{"address_type": "other", "used_at": {"Time": "0001-01-01T00:00:00Z", "Valid": false}}`, 0, CredentialsTypeCodeAuth, 1) + }) + + t.Run("from=v2 with empty value", func(t *testing.T) { + run(t, []string{"foo@example.org", "+12341234"}, `{"addresses": [{"address":"foo@example.org","channel":"email"},{"address":"+12341234","channel":"sms"}]}`, 1, CredentialsTypeCodeAuth, 1) + }) + }) + t.Run("type=webauthn", func(t *testing.T) { t.Run("from=v0", func(t *testing.T) { - i := &Identity{ - ID: identityID, - Credentials: map[CredentialsType]Credentials{ - CredentialsTypeWebAuthn: { - Identifiers: []string{"4d64fa08-20fc-450d-bebd-ebd7c7b6e249"}, - Type: CredentialsTypeWebAuthn, - Version: 0, - Config: webAuthnV0, - }}, - } - - require.NoError(t, UpgradeCredentials(i)) - wc := WithCredentialsAndAdminMetadataInJSON(*i) - snapshotx.SnapshotTExcept(t, &wc, nil) - - assert.Equal(t, 1, i.Credentials[CredentialsTypeWebAuthn].Version) + run(t, []string{"4d64fa08-20fc-450d-bebd-ebd7c7b6e249"}, string(webAuthnV0), 0, CredentialsTypeWebAuthn, 1) }) t.Run("from=v1", func(t *testing.T) { - i := &Identity{ - ID: identityID, - Credentials: map[CredentialsType]Credentials{ - CredentialsTypeWebAuthn: { - Type: CredentialsTypeWebAuthn, - Version: 1, - Config: webAuthnV1, - }}, - } - - require.NoError(t, UpgradeCredentials(i)) - wc := WithCredentialsAndAdminMetadataInJSON(*i) - snapshotx.SnapshotTExcept(t, &wc, nil) - - assert.Equal(t, 1, i.Credentials[CredentialsTypeWebAuthn].Version) + + run(t, []string{}, string(webAuthnV1), 1, CredentialsTypeWebAuthn, 1) }) }) } diff --git a/identity/credentials_oidc.go b/identity/credentials_oidc.go index 09b5d0aecad0..27462f927024 100644 --- a/identity/credentials_oidc.go +++ b/identity/credentials_oidc.go @@ -32,8 +32,36 @@ type CredentialsOIDCProvider struct { Organization string `json:"organization,omitempty"` } +// swagger:ignore +type CredentialsOIDCEncryptedTokens struct { + RefreshToken string `json:"refresh_token,omitempty"` + IDToken string `json:"id_token,omitempty"` + AccessToken string `json:"access_token,omitempty"` +} + +func (c *CredentialsOIDCEncryptedTokens) GetRefreshToken() string { + if c == nil { + return "" + } + return c.RefreshToken +} + +func (c *CredentialsOIDCEncryptedTokens) GetAccessToken() string { + if c == nil { + return "" + } + return c.AccessToken +} + +func (c *CredentialsOIDCEncryptedTokens) GetIDToken() string { + if c == nil { + return "" + } + return c.IDToken +} + // NewCredentialsOIDC creates a new OIDC credential. -func NewCredentialsOIDC(idToken, accessToken, refreshToken, provider, subject, organization string) (*Credentials, error) { +func NewCredentialsOIDC(tokens *CredentialsOIDCEncryptedTokens, provider, subject, organization string) (*Credentials, error) { if provider == "" { return nil, errors.New("received empty provider in oidc credentials") } @@ -48,9 +76,9 @@ func NewCredentialsOIDC(idToken, accessToken, refreshToken, provider, subject, o { Subject: subject, Provider: provider, - InitialIDToken: idToken, - InitialAccessToken: accessToken, - InitialRefreshToken: refreshToken, + InitialIDToken: tokens.GetIDToken(), + InitialAccessToken: tokens.GetAccessToken(), + InitialRefreshToken: tokens.GetRefreshToken(), Organization: organization, }}, }); err != nil { @@ -65,6 +93,14 @@ func NewCredentialsOIDC(idToken, accessToken, refreshToken, provider, subject, o }, nil } +func (c *CredentialsOIDCProvider) GetTokens() *CredentialsOIDCEncryptedTokens { + return &CredentialsOIDCEncryptedTokens{ + RefreshToken: c.InitialRefreshToken, + IDToken: c.InitialIDToken, + AccessToken: c.InitialAccessToken, + } +} + func OIDCUniqueID(provider, subject string) string { return fmt.Sprintf("%s:%s", provider, subject) } diff --git a/identity/credentials_oidc_test.go b/identity/credentials_oidc_test.go index dda20f73deb7..f52a077d107c 100644 --- a/identity/credentials_oidc_test.go +++ b/identity/credentials_oidc_test.go @@ -10,10 +10,10 @@ import ( ) func TestNewCredentialsOIDC(t *testing.T) { - _, err := NewCredentialsOIDC("", "", "", "", "not-empty", "") + _, err := NewCredentialsOIDC(new(CredentialsOIDCEncryptedTokens), "", "not-empty", "") require.Error(t, err) - _, err = NewCredentialsOIDC("", "", "", "not-empty", "", "") + _, err = NewCredentialsOIDC(new(CredentialsOIDCEncryptedTokens), "not-empty", "", "") require.Error(t, err) - _, err = NewCredentialsOIDC("", "", "", "not-empty", "not-empty", "") + _, err = NewCredentialsOIDC(new(CredentialsOIDCEncryptedTokens), "not-empty", "not-empty", "") require.NoError(t, err) } diff --git a/identity/credentials_password.go b/identity/credentials_password.go index 85f5ac1d7d0c..4a6e7ebc7144 100644 --- a/identity/credentials_password.go +++ b/identity/credentials_password.go @@ -9,4 +9,13 @@ package identity type CredentialsPassword struct { // HashedPassword is a hash-representation of the password. HashedPassword string `json:"hashed_password"` + + // UsePasswordMigrationHook is set to true if the password should be migrated + // using the password migration hook. If set, and the HashedPassword is empty, a + // webhook will be called during login to migrate the password. + UsePasswordMigrationHook bool `json:"use_password_migration_hook,omitempty"` +} + +func (cp *CredentialsPassword) ShouldUsePasswordMigrationHook() bool { + return cp != nil && cp.HashedPassword == "" && cp.UsePasswordMigrationHook } diff --git a/identity/credentials_password_test.go b/identity/credentials_password_test.go new file mode 100644 index 000000000000..6e62720779e6 --- /dev/null +++ b/identity/credentials_password_test.go @@ -0,0 +1,46 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCredentialsPassword_ShouldUsePasswordMigrationHook(t *testing.T) { + tests := []struct { + name string + cp *CredentialsPassword + want bool + }{{ + name: "pw set", + cp: &CredentialsPassword{ + HashedPassword: "pw", + UsePasswordMigrationHook: true, + }, + want: false, + }, { + name: "pw not set", + cp: &CredentialsPassword{ + HashedPassword: "", + UsePasswordMigrationHook: true, + }, + want: true, + }, { + name: "nil", + want: false, + }, { + name: "pw not set, hook not set", + cp: &CredentialsPassword{ + HashedPassword: "", + }, + want: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, tt.cp.ShouldUsePasswordMigrationHook(), "ShouldUsePasswordMigrationHook()") + }) + } +} diff --git a/identity/credentials_webauthn.go b/identity/credentials_webauthn.go index ecfd132ea0b1..ae6b2e34cb77 100644 --- a/identity/credentials_webauthn.go +++ b/identity/credentials_webauthn.go @@ -6,10 +6,14 @@ package identity import ( "time" + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + + "github.com/ory/kratos/x/webauthnx/aaguid" ) -// CredentialsConfig is the struct that is being used as part of the identity credentials. +// CredentialsWebAuthnConfig is the struct that is being used as part of the identity credentials. type CredentialsWebAuthnConfig struct { // List of webauthn credentials. Credentials CredentialsWebAuthn `json:"credentials"` @@ -19,17 +23,30 @@ type CredentialsWebAuthnConfig struct { type CredentialsWebAuthn []CredentialWebAuthn func CredentialFromWebAuthn(credential *webauthn.Credential, isPasswordless bool) *CredentialWebAuthn { - return &CredentialWebAuthn{ + cred := &CredentialWebAuthn{ ID: credential.ID, PublicKey: credential.PublicKey, IsPasswordless: isPasswordless, AttestationType: credential.AttestationType, - Authenticator: AuthenticatorWebAuthn{ + AddedAt: time.Now().UTC().Round(time.Second), + Authenticator: &AuthenticatorWebAuthn{ AAGUID: credential.Authenticator.AAGUID, SignCount: credential.Authenticator.SignCount, CloneWarning: credential.Authenticator.CloneWarning, }, + Flags: &CredentialWebAuthnFlags{ + UserPresent: credential.Flags.UserPresent, + UserVerified: credential.Flags.UserVerified, + BackupEligible: credential.Flags.BackupEligible, + BackupState: credential.Flags.BackupState, + }, + } + id := aaguid.Lookup(credential.Authenticator.AAGUID) + if id != nil { + cred.DisplayName = id.Name } + + return cred } func (c CredentialsWebAuthn) ToWebAuthn() (result []webauthn.Credential) { @@ -39,40 +56,112 @@ func (c CredentialsWebAuthn) ToWebAuthn() (result []webauthn.Credential) { return result } -func (c CredentialsWebAuthn) ToWebAuthnFiltered(aal AuthenticatorAssuranceLevel) (result []webauthn.Credential) { +// PasswordlessOnly returns only passwordless credentials. +func (c CredentialsWebAuthn) PasswordlessOnly(authenticatorResponseFlags *protocol.AuthenticatorFlags) (result []webauthn.Credential) { for k, cc := range c { - if aal == AuthenticatorAssuranceLevel1 && !cc.IsPasswordless { - continue - } else if aal == AuthenticatorAssuranceLevel2 && cc.IsPasswordless { - continue + // Upgrade path for legacy webauthn credentials. Only possible if we are handling a response from an authenticator. + if c[k].Flags == nil && authenticatorResponseFlags != nil { + c[k].Flags = &CredentialWebAuthnFlags{ + BackupEligible: authenticatorResponseFlags.HasBackupEligible(), + BackupState: authenticatorResponseFlags.HasBackupState(), + } } - result = append(result, *c[k].ToWebAuthn()) + if cc.IsPasswordless { + result = append(result, *c[k].ToWebAuthn()) + } + } + return result +} + +// ToWebAuthnFiltered returns only the appropriate credentials for the requested +// AAL. For AAL1, only passwordless credentials are returned, for AAL2, only +// non-passwordless credentials are returned. +// +// authenticatorResponseFlags should be passed if the response is from an authenticator. It will be used to +// upgrade legacy webauthn credentials' BackupEligible and BackupState flags. +func (c CredentialsWebAuthn) ToWebAuthnFiltered(aal AuthenticatorAssuranceLevel, authenticatorResponseFlags *protocol.AuthenticatorFlags) (result []webauthn.Credential) { + for k, cc := range c { + // Upgrade path for legacy webauthn credentials. Only possible if we are handling a response from an authenticator. + if c[k].Flags == nil && authenticatorResponseFlags != nil { + c[k].Flags = &CredentialWebAuthnFlags{ + BackupEligible: authenticatorResponseFlags.HasBackupEligible(), + BackupState: authenticatorResponseFlags.HasBackupState(), + } + } + + if (aal == AuthenticatorAssuranceLevel1 && cc.IsPasswordless) || + (aal == AuthenticatorAssuranceLevel2 && !cc.IsPasswordless) { + result = append(result, *c[k].ToWebAuthn()) + } } return result } func (c *CredentialWebAuthn) ToWebAuthn() *webauthn.Credential { - return &webauthn.Credential{ + wc := &webauthn.Credential{ ID: c.ID, PublicKey: c.PublicKey, AttestationType: c.AttestationType, - Authenticator: webauthn.Authenticator{ + Transport: c.Transport, + } + + if c.Authenticator != nil { + wc.Authenticator = webauthn.Authenticator{ AAGUID: c.Authenticator.AAGUID, SignCount: c.Authenticator.SignCount, CloneWarning: c.Authenticator.CloneWarning, - }, + } } + + if c.Flags != nil { + wc.Flags = webauthn.CredentialFlags{ + UserPresent: c.Flags.UserPresent, + UserVerified: c.Flags.UserVerified, + BackupEligible: c.Flags.BackupEligible, + BackupState: c.Flags.BackupState, + } + } + + if c.Attestation != nil { + wc.Attestation = webauthn.CredentialAttestation{ + ClientDataJSON: c.Attestation.ClientDataJSON, + ClientDataHash: c.Attestation.ClientDataHash, + AuthenticatorData: c.Attestation.AuthenticatorData, + PublicKeyAlgorithm: c.Attestation.PublicKeyAlgorithm, + Object: c.Attestation.Object, + } + } + + return wc } type CredentialWebAuthn struct { - ID []byte `json:"id"` - PublicKey []byte `json:"public_key"` - AttestationType string `json:"attestation_type"` - Authenticator AuthenticatorWebAuthn `json:"authenticator"` - DisplayName string `json:"display_name"` - AddedAt time.Time `json:"added_at"` - IsPasswordless bool `json:"is_passwordless"` + ID []byte `json:"id"` + PublicKey []byte `json:"public_key"` + AttestationType string `json:"attestation_type"` + Authenticator *AuthenticatorWebAuthn `json:"authenticator,omitempty"` + DisplayName string `json:"display_name"` + AddedAt time.Time `json:"added_at"` + IsPasswordless bool `json:"is_passwordless"` + Flags *CredentialWebAuthnFlags `json:"flags,omitempty"` + Transport []protocol.AuthenticatorTransport `json:"transport,omitempty"` + Attestation *CredentialWebAuthnAttestation `json:"attestation,omitempty"` +} + +type CredentialWebAuthnFlags struct { + UserPresent bool `json:"user_present"` + UserVerified bool `json:"user_verified"` + BackupEligible bool `json:"backup_eligible"` + BackupState bool `json:"backup_state"` +} + +type CredentialWebAuthnAttestation struct { + ClientDataJSON []byte `json:"client_dataJSON"` + ClientDataHash []byte `json:"client_data_hash"` + AuthenticatorData []byte `json:"authenticator_data"` + PublicKeyAlgorithm int64 `json:"public_key_algorithm"` + Object []byte `json:"object"` } type AuthenticatorWebAuthn struct { diff --git a/identity/credentials_webauthn_test.go b/identity/credentials_webauthn_test.go index c081c1826c41..8918898e71be 100644 --- a/identity/credentials_webauthn_test.go +++ b/identity/credentials_webauthn_test.go @@ -6,7 +6,10 @@ package identity import ( "testing" + "github.com/stretchr/testify/require" + "github.com/go-webauthn/webauthn/webauthn" + "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" ) @@ -25,20 +28,37 @@ func TestCredentialConversion(t *testing.T) { actual := CredentialFromWebAuthn(expected, false).ToWebAuthn() assert.Equal(t, expected, actual) - actualList := CredentialsWebAuthn{*CredentialFromWebAuthn(expected, false)}.ToWebAuthnFiltered(AuthenticatorAssuranceLevel2) + actualList := CredentialsWebAuthn{*CredentialFromWebAuthn(expected, false)}.ToWebAuthnFiltered(AuthenticatorAssuranceLevel2, nil) assert.Equal(t, []webauthn.Credential{*expected}, actualList) - actualList = CredentialsWebAuthn{*CredentialFromWebAuthn(expected, true)}.ToWebAuthnFiltered(AuthenticatorAssuranceLevel1) + actualList = CredentialsWebAuthn{*CredentialFromWebAuthn(expected, true)}.ToWebAuthnFiltered(AuthenticatorAssuranceLevel1, nil) assert.Equal(t, []webauthn.Credential{*expected}, actualList) - actualList = CredentialsWebAuthn{*CredentialFromWebAuthn(expected, true)}.ToWebAuthnFiltered(AuthenticatorAssuranceLevel2) + actualList = CredentialsWebAuthn{*CredentialFromWebAuthn(expected, true)}.ToWebAuthnFiltered(AuthenticatorAssuranceLevel2, nil) assert.Len(t, actualList, 0) - actualList = CredentialsWebAuthn{*CredentialFromWebAuthn(expected, false)}.ToWebAuthnFiltered(AuthenticatorAssuranceLevel1) + actualList = CredentialsWebAuthn{*CredentialFromWebAuthn(expected, false)}.ToWebAuthnFiltered(AuthenticatorAssuranceLevel1, nil) assert.Len(t, actualList, 0) fromWebAuthn := CredentialFromWebAuthn(expected, true) assert.True(t, fromWebAuthn.IsPasswordless) fromWebAuthn = CredentialFromWebAuthn(expected, false) assert.False(t, fromWebAuthn.IsPasswordless) + + expected.Authenticator.AAGUID = uuid.Must(uuid.FromString("ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4")).Bytes() + fromWebAuthn = CredentialFromWebAuthn(expected, false) + assert.Equal(t, "Google Password Manager", fromWebAuthn.DisplayName) +} + +func TestPasswordlessOnly(t *testing.T) { + a := *CredentialFromWebAuthn(&webauthn.Credential{ID: []byte("a")}, false) + b := *CredentialFromWebAuthn(&webauthn.Credential{ID: []byte("b")}, false) + c := *CredentialFromWebAuthn(&webauthn.Credential{ID: []byte("c")}, true) + d := *CredentialFromWebAuthn(&webauthn.Credential{ID: []byte("d")}, false) + e := *CredentialFromWebAuthn(&webauthn.Credential{ID: []byte("e")}, true) + expected := CredentialsWebAuthn{a, b, c, d, e} + + actual := expected.PasswordlessOnly(nil) + require.Len(t, actual, 2) + assert.Equal(t, []webauthn.Credential{*c.ToWebAuthn(), *e.ToWebAuthn()}, actual) } diff --git a/identity/extension_credentials.go b/identity/extension_credentials.go index 4ac0940e4b75..586d01231fe7 100644 --- a/identity/extension_credentials.go +++ b/identity/extension_credentials.go @@ -4,22 +4,28 @@ package identity import ( + "encoding/json" "fmt" + "sort" "strings" "sync" + "github.com/pkg/errors" + "github.com/samber/lo" + + "github.com/ory/kratos/x" + "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/schema" "github.com/ory/x/sqlxx" "github.com/ory/x/stringslice" - "github.com/ory/x/stringsx" - - "github.com/ory/kratos/schema" ) type SchemaExtensionCredentials struct { - i *Identity - v map[CredentialsType][]string - l sync.Mutex + i *Identity + v map[CredentialsType][]string + addresses []CredentialsCodeAddress + l sync.Mutex // fandom-start caseSensitiveIds bool // fandom-end @@ -38,6 +44,7 @@ func (r *SchemaExtensionCredentials) setIdentifier(ct CredentialsType, value int Config: sqlxx.JSONRawMessage{}, } } + if r.v == nil { r.v = make(map[CredentialsType][]string) } @@ -67,22 +74,62 @@ func (r *SchemaExtensionCredentials) Run(ctx jsonschema.ValidationContext, s sch } if s.Credentials.Code.Identifier { - switch f := stringsx.SwitchExact(s.Credentials.Code.Via); { - case f.AddCase(AddressTypeEmail): - if !jsonschema.Formats["email"](value) { - return ctx.Error("format", "%q is not a valid %q", value, s.Credentials.Code.Via) + via, err := NewCodeChannel(s.Credentials.Code.Via) + if err != nil { + return ctx.Error("ory.sh~/kratos/credentials/code/via", "channel type %q must be one of %s", s.Credentials.Code.Via, strings.Join([]string{ + string(CodeChannelEmail), + string(CodeChannelSMS), + }, ", ")) + } + + cred := r.i.GetCredentialsOr(CredentialsTypeCodeAuth, &Credentials{ + Type: CredentialsTypeCodeAuth, + Identifiers: []string{}, + Config: sqlxx.JSONRawMessage("{}"), + Version: 1, + }) + + var conf CredentialsCode + conf.Addresses = r.addresses + value, err := x.NormalizeIdentifier(fmt.Sprintf("%s", value), string(via)) + if err != nil { + return &jsonschema.ValidationError{Message: err.Error()} + } + + conf.Addresses = append(conf.Addresses, CredentialsCodeAddress{ + Channel: via, + Address: value, + }) + + conf.Addresses = lo.UniqBy(conf.Addresses, func(item CredentialsCodeAddress) string { + return fmt.Sprintf("%x:%s", item.Address, item.Channel) + }) + + sort.SliceStable(conf.Addresses, func(i, j int) bool { + if conf.Addresses[i].Address == conf.Addresses[j].Address { + return conf.Addresses[i].Channel < conf.Addresses[j].Channel } + return conf.Addresses[i].Address < conf.Addresses[j].Address + }) - r.setIdentifier(CredentialsTypeCodeAuth, value) - // case f.AddCase(AddressTypePhone): - // if !jsonschema.Formats["tel"](value) { - // return ctx.Error("format", "%q is not a valid %q", value, s.Credentials.Code.Via) - // } + if r.v == nil { + r.v = make(map[CredentialsType][]string) + } - // r.setIdentifier(CredentialsTypeCodeAuth, value, CredentialsIdentifierAddressTypePhone) - default: - return ctx.Error("", "credentials.code.via has unknown value %q", s.Credentials.Code.Via) + r.v[CredentialsTypeCodeAuth] = stringslice.Unique(append(r.v[CredentialsTypeCodeAuth], + lo.Map(conf.Addresses, func(item CredentialsCodeAddress, _ int) string { + return item.Address + })..., + )) + r.addresses = conf.Addresses + + cred.Identifiers = r.v[CredentialsTypeCodeAuth] + cred.Config, err = json.Marshal(conf) + if err != nil { + return errors.WithStack(err) } + + r.i.SetCredentials(CredentialsTypeCodeAuth, *cred) } return nil diff --git a/identity/extension_credentials_test.go b/identity/extension_credentials_test.go index 3b63ffb633dd..1a79a6375de9 100644 --- a/identity/extension_credentials_test.go +++ b/identity/extension_credentials_test.go @@ -9,6 +9,10 @@ import ( "fmt" "testing" + "github.com/ory/x/sqlxx" + + "github.com/ory/x/snapshotx" + "github.com/ory/jsonschema/v3" _ "github.com/ory/jsonschema/v3/fileloader" @@ -23,70 +27,130 @@ var ctx = context.Background() func TestSchemaExtensionCredentials(t *testing.T) { for k, tc := range []struct { - expectErr error - schema string - doc string - expect []string - existing *identity.Credentials - ct identity.CredentialsType + expectErr error + schema string + doc string + expectedIdentifiers []string + existing *identity.Credentials + ct identity.CredentialsType }{ { - doc: `{"email":"foo@ory.sh"}`, - schema: "file://./stub/extension/credentials/schema.json", - expect: []string{"foo@ory.sh"}, - ct: identity.CredentialsTypePassword, + doc: `{"email":"foo@ory.sh"}`, + schema: "file://./stub/extension/credentials/schema.json", + expectedIdentifiers: []string{"foo@ory.sh"}, + ct: identity.CredentialsTypePassword, }, { - doc: `{"emails":["foo@ory.sh","foo@ory.sh","bar@ory.sh"], "username": "foobar"}`, - schema: "file://./stub/extension/credentials/multi.schema.json", - expect: []string{"foo@ory.sh", "bar@ory.sh", "foobar"}, - ct: identity.CredentialsTypePassword, + doc: `{"emails":["foo@ory.sh","foo@ory.sh","bar@ory.sh"], "username": "foobar"}`, + schema: "file://./stub/extension/credentials/multi.schema.json", + expectedIdentifiers: []string{"foo@ory.sh", "bar@ory.sh", "foobar"}, + ct: identity.CredentialsTypePassword, }, { - doc: `{"emails":["foo@ory.sh","foo@ory.sh","bar@ory.sh"], "username": "foobar"}`, - schema: "file://./stub/extension/credentials/multi.schema.json", - expect: []string{"foo@ory.sh", "bar@ory.sh"}, - ct: identity.CredentialsTypeWebAuthn, + doc: `{"emails":["foo@ory.sh","foo@ory.sh","bar@ory.sh"], "username": "foobar"}`, + schema: "file://./stub/extension/credentials/multi.schema.json", + expectedIdentifiers: []string{"foo@ory.sh", "bar@ory.sh"}, + ct: identity.CredentialsTypeWebAuthn, }, { - doc: `{"emails":["FOO@ory.sh","bar@ory.sh"], "username": "foobar"}`, - schema: "file://./stub/extension/credentials/multi.schema.json", - expect: []string{"foo@ory.sh", "bar@ory.sh", "foobar"}, + doc: `{"emails":["FOO@ory.sh","bar@ory.sh"], "username": "foobar"}`, + schema: "file://./stub/extension/credentials/multi.schema.json", + expectedIdentifiers: []string{"foo@ory.sh", "bar@ory.sh", "foobar"}, existing: &identity.Credentials{ Identifiers: []string{"not-foo@ory.sh"}, }, ct: identity.CredentialsTypePassword, }, { - doc: `{"email":"foo@ory.sh"}`, - schema: "file://./stub/extension/credentials/webauthn.schema.json", - expect: []string{"foo@ory.sh"}, - ct: identity.CredentialsTypeWebAuthn, + doc: `{"email":"foo@ory.sh"}`, + schema: "file://./stub/extension/credentials/webauthn.schema.json", + expectedIdentifiers: []string{"foo@ory.sh"}, + ct: identity.CredentialsTypeWebAuthn, }, { - doc: `{"email":"FOO@ory.sh"}`, - schema: "file://./stub/extension/credentials/webauthn.schema.json", - expect: []string{"foo@ory.sh"}, + doc: `{"email":"FOO@ory.sh"}`, + schema: "file://./stub/extension/credentials/webauthn.schema.json", + expectedIdentifiers: []string{"foo@ory.sh"}, existing: &identity.Credentials{ Identifiers: []string{"not-foo@ory.sh"}, }, ct: identity.CredentialsTypeWebAuthn, }, { - doc: `{"email":"foo@ory.sh"}`, - schema: "file://./stub/extension/credentials/code.schema.json", - expect: []string{"foo@ory.sh"}, - ct: identity.CredentialsTypeCodeAuth, + doc: `{"email":"foo@ory.sh"}`, + schema: "file://./stub/extension/credentials/code.schema.json", + expectedIdentifiers: []string{"foo@ory.sh"}, + ct: identity.CredentialsTypeCodeAuth, }, { - doc: `{"email":"FOO@ory.sh"}`, - schema: "file://./stub/extension/credentials/code.schema.json", - expect: []string{"foo@ory.sh"}, + doc: `{"email":"FOO@ory.sh"}`, + schema: "file://./stub/extension/credentials/code.schema.json", + expectedIdentifiers: []string{"foo@ory.sh"}, existing: &identity.Credentials{ Identifiers: []string{"not-foo@ory.sh"}, }, ct: identity.CredentialsTypeCodeAuth, }, + { + doc: `{"email":"FOO@ory.sh"}`, + schema: "file://./stub/extension/credentials/code.schema.json", + expectedIdentifiers: []string{"foo@ory.sh"}, + existing: &identity.Credentials{ + Identifiers: []string{"not-foo@ory.sh", "foo@ory.sh"}, + Config: sqlxx.JSONRawMessage(`{"addresses":[{"channel":"email","address":"not-foo@ory.sh"}]}`), + }, + ct: identity.CredentialsTypeCodeAuth, + }, + { + doc: `{"email":"FOO@ory.sh","phone":"+49 176 671 11 638"}`, + schema: "file://./stub/extension/credentials/code-phone-email.schema.json", + expectedIdentifiers: []string{"+4917667111638", "foo@ory.sh"}, + existing: &identity.Credentials{ + Identifiers: []string{"not-foo@ory.sh", "foo@ory.sh"}, + Config: sqlxx.JSONRawMessage(`{"addresses":[{"channel":"email","address":"not-foo@ory.sh"}]}`), + }, + ct: identity.CredentialsTypeCodeAuth, + }, + { + doc: `{"email":"FOO@ory.sh","phone":"+49 176 671 11 638"}`, + schema: "file://./stub/extension/credentials/code-phone-email.schema.json", + expectedIdentifiers: []string{"+4917667111638", "foo@ory.sh"}, + existing: &identity.Credentials{ + Identifiers: []string{"not-foo@ory.sh", "foo@ory.sh"}, + Config: sqlxx.JSONRawMessage(`{"addresses":[{"channel":"email","address":"not-foo@ory.sh"}]}`), + }, + ct: identity.CredentialsTypeCodeAuth, + }, + { + doc: `{"email":"FOO@ory.sh","email2":"FOO@ory.sh","phone":"+49 176 671 11 638"}`, + schema: "file://./stub/extension/credentials/code-phone-email.schema.json", + expectedIdentifiers: []string{"+4917667111638", "foo@ory.sh"}, + existing: &identity.Credentials{ + Identifiers: []string{"not-foo@ory.sh", "fOo@ory.sh"}, + Config: sqlxx.JSONRawMessage(`{"addresses":[{"channel":"email","address":"not-foo@ory.sh"}]}`), + }, + ct: identity.CredentialsTypeCodeAuth, + }, + { + doc: `{"email":"FOO@ory.sh","email2":"FOO@ory.sh","email3":"bar@ory.sh","phone":"+49 176 671 11 638"}`, + schema: "file://./stub/extension/credentials/code-phone-email.schema.json", + expectedIdentifiers: []string{"+4917667111638", "foo@ory.sh", "bar@ory.sh"}, + existing: &identity.Credentials{ + Identifiers: []string{"not-foo@ory.sh", "fOo@ory.sh"}, + Config: sqlxx.JSONRawMessage(`{"addresses":[{"channel":"email","address":"not-foo@ory.sh"}]}`), + }, + ct: identity.CredentialsTypeCodeAuth, + }, + { + doc: `{"email":"bar@ory.sh"}`, + schema: "file://./stub/extension/credentials/email.schema.json", + expectedIdentifiers: []string{"bar@ory.sh"}, + existing: &identity.Credentials{ + Identifiers: []string{"foo@ory.sh"}, + Config: sqlxx.JSONRawMessage(`{"addresses":[{"channel":"email","address":"foo@ory.sh"}]}`), + }, + ct: identity.CredentialsTypeCodeAuth, + }, } { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { c := jsonschema.NewCompiler() @@ -105,12 +169,15 @@ func TestSchemaExtensionCredentials(t *testing.T) { err = c.MustCompile(ctx, tc.schema).Validate(bytes.NewBufferString(tc.doc)) if tc.expectErr != nil { require.EqualError(t, err, tc.expectErr.Error()) + } else { + require.NoError(t, err) } require.NoError(t, e.Finish()) credentials, ok := i.GetCredentials(tc.ct) require.True(t, ok) - assert.ElementsMatch(t, tc.expect, credentials.Identifiers) + assert.ElementsMatch(t, tc.expectedIdentifiers, credentials.Identifiers) + snapshotx.SnapshotT(t, credentials, snapshotx.ExceptPaths("identifiers")) }) } } diff --git a/identity/extension_verification_test.go b/identity/extension_verification_test.go index ebf2c09f207e..cc7f47b07091 100644 --- a/identity/extension_verification_test.go +++ b/identity/extension_verification_test.go @@ -369,6 +369,21 @@ func TestSchemaExtensionVerification(t *testing.T) { }, }, }, + { + // see https://github.com/ory/kratos/issues/3933 + name: "phone:should parse +16453331111", + schema: phoneSchemaPath, + doc: `{"phones":["+16453331111"]}`, + expect: []VerifiableAddress{ + { + Value: "+16453331111", + Verified: false, + Status: VerifiableAddressStatusPending, + Via: ChannelTypeSMS, + IdentityID: iid, + }, + }, + }, } { t.Run(fmt.Sprintf("case=%v", tc.name), func(t *testing.T) { id := &Identity{ID: iid, VerifiableAddresses: tc.existing} @@ -385,6 +400,8 @@ func TestSchemaExtensionVerification(t *testing.T) { if tc.expectErr != nil { require.EqualError(t, err, tc.expectErr.Error()) return + } else { + require.NoError(t, err) } require.NoError(t, e.Finish()) diff --git a/identity/handler.go b/identity/handler.go index 84e87c25b984..47c6dcb8e06c 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -11,6 +11,8 @@ import ( "strings" "time" + "github.com/gofrs/uuid" + "github.com/ory/x/crdbx" "github.com/ory/x/pagination/keysetpagination" @@ -168,6 +170,15 @@ type listIdentitiesParameters struct { // in: query CredentialsIdentifierSimilar string `json:"preview_credentials_identifier_similar"` + // Include Credentials in Response + // + // Include any credential, for example `password` or `oidc`, in the response. When set to `oidc`, This will return + // the initial OAuth 2.0 Access Token, OAuth 2.0 Refresh Token and the OpenID Connect ID Token if available. + // + // required: false + // in: query + DeclassifyCredentials []string `json:"include_credential"` + crdbx.ConsistencyRequestParameters } @@ -189,21 +200,42 @@ type listIdentitiesParameters struct { // 200: listIdentities // default: errorGeneric func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - var ( - err error - params = ListIdentityParameters{ - Expand: ExpandDefault, - IdsFilter: r.URL.Query()["ids"], - CredentialsIdentifier: r.URL.Query().Get("credentials_identifier"), - CredentialsIdentifierSimilar: r.URL.Query().Get("preview_credentials_identifier_similar"), - ConsistencyLevel: crdbx.ConsistencyLevelFromRequest(r), + includeCredentials := r.URL.Query()["include_credential"] + var err error + var declassify []CredentialsType + for _, v := range includeCredentials { + tc, ok := ParseCredentialsType(v) + if ok { + declassify = append(declassify, tc) + } else { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Invalid value `%s` for parameter `include_credential`.", declassify))) + return } - ) + } + + var idsFilter []uuid.UUID + for _, v := range r.URL.Query()["ids"] { + id, err := uuid.FromString(v) + if err != nil { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Invalid UUID value `%s` for parameter `ids`.", v))) + return + } + idsFilter = append(idsFilter, id) + } + + params := ListIdentityParameters{ + Expand: ExpandDefault, + IdsFilter: idsFilter, + CredentialsIdentifier: r.URL.Query().Get("credentials_identifier"), + CredentialsIdentifierSimilar: r.URL.Query().Get("preview_credentials_identifier_similar"), + ConsistencyLevel: crdbx.ConsistencyLevelFromRequest(r), + DeclassifyCredentials: declassify, + } if params.CredentialsIdentifier != "" && params.CredentialsIdentifierSimilar != "" { h.r.Writer().WriteError(w, r, herodot.ErrBadRequest.WithReason("Cannot pass both credentials_identifier and preview_credentials_identifier_similar.")) return } - if params.CredentialsIdentifier != "" || params.CredentialsIdentifierSimilar != "" { + if params.CredentialsIdentifier != "" || params.CredentialsIdentifierSimilar != "" || len(params.DeclassifyCredentials) > 0 { params.Expand = ExpandEverything } params.KeySetPagination, params.PagePagination, err = x.ParseKeysetOrPagePagination(r) @@ -235,9 +267,15 @@ func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Para } // Identities using the marshaler for including metadata_admin - isam := make([]WithCredentialsMetadataAndAdminMetadataInJSON, len(is)) + isam := make([]WithCredentialsAndAdminMetadataInJSON, len(is)) for i, identity := range is { - isam[i] = WithCredentialsMetadataAndAdminMetadataInJSON(identity) + emit, err := identity.WithDeclassifiedCredentials(r.Context(), h.r, params.DeclassifyCredentials) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + isam[i] = WithCredentialsAndAdminMetadataInJSON(*emit) } h.r.Writer().Write(w, r, isam) @@ -402,6 +440,9 @@ type AdminIdentityImportCredentialsPasswordConfig struct { // The password in plain text if no hash is available. Password string `json:"password"` + + // If set to true, the password will be migrated using the password migration hook. + UsePasswordMigrationHook bool `json:"use_password_migration_hook,omitempty"` } // Create Identity and Import Social Sign In Credentials @@ -528,9 +569,9 @@ func (h *Handler) identityFromCreateIdentityBody(ctx context.Context, cr *Create // swagger:route PATCH /admin/identities identity batchPatchIdentities // -// # Create and deletes multiple identities +// # Create multiple identities // -// Creates or delete multiple +// Creates multiple // [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model). // This endpoint can also be used to [import // credentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities) @@ -592,13 +633,22 @@ func (h *Handler) batchPatchIdentities(w http.ResponseWriter, r *http.Request, _ } } - if err := h.r.IdentityManager().CreateIdentities(r.Context(), identities); err != nil { + err := h.r.IdentityManager().CreateIdentities(r.Context(), identities) + partialErr := new(CreateIdentitiesError) + if err != nil && !errors.As(err, &partialErr) { h.r.Writer().WriteError(w, r, err) return } for resIdx, identitiesIdx := range indexInIdentities { if identitiesIdx != nil { - res.Identities[resIdx].IdentityID = &identities[*identitiesIdx].ID + ident := identities[*identitiesIdx] + // Check if the identity was created successfully. + if failed := partialErr.Find(ident); failed != nil { + res.Identities[resIdx].Action = ActionError + res.Identities[resIdx].Error = failed.Error + } else { + res.Identities[resIdx].IdentityID = &ident.ID + } } } @@ -923,83 +973,50 @@ func (h *Handler) patch(w http.ResponseWriter, r *http.Request, ps httprouter.Pa patchedIdentity.StateChangedAt = &stateChangedAt } - updatedIdenty := Identity(patchedIdentity) + updatedIdentity := Identity(patchedIdentity) if err := h.r.IdentityManager().Update( r.Context(), - &updatedIdenty, + &updatedIdentity, ManagerAllowWriteProtectedTraits, ); err != nil { h.r.Writer().WriteError(w, r, err) return } - h.r.Writer().Write(w, r, WithCredentialsMetadataAndAdminMetadataInJSON(updatedIdenty)) -} - -func deletCredentialWebAuthFromIdentity(identity *Identity) (*Identity, error) { - cred, ok := identity.GetCredentials(CredentialsTypeWebAuthn) - if !ok { - // This should never happend as it's checked earlier in the code; - // But we never know... - return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf("You tried to remove a CredentialsTypeWebAuthn but this user have no CredentialsTypeWebAuthn set up.")) - } - - var cc CredentialsWebAuthnConfig - if err := json.Unmarshal(cred.Config, &cc); err != nil { - // Database has been tampered or the json schema are incompatible (migration issue); - return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error())) - } - - updated := make([]CredentialWebAuthn, 0) - for k, cred := range cc.Credentials { - if cred.IsPasswordless { - updated = append(updated, cc.Credentials[k]) - } - } - - if len(updated) == 0 { - identity.DeleteCredentialsType(CredentialsTypeWebAuthn) - return identity, nil - } - - cc.Credentials = updated - message, err := json.Marshal(cc) - if err != nil { - return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error())) - } - - cred.Config = message - identity.SetCredentials(CredentialsTypeWebAuthn, *cred) - return identity, nil + h.r.Writer().Write(w, r, WithCredentialsMetadataAndAdminMetadataInJSON(updatedIdentity)) } // Delete Credential Parameters // // swagger:parameters deleteIdentityCredentials -// -//nolint:deadcode,unused -//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions -type deleteIdentityCredentials struct { +type _ struct { // ID is the identity's ID. // // required: true // in: path ID string `json:"id"` - // Type is the type of credentials to be deleted. + // Type is the type of credentials to delete. // // required: true // in: path Type CredentialsType `json:"type"` + + // Identifier is the identifier of the OIDC credential to delete. + // Find the identifier by calling the `GET /admin/identities/{id}?include_credential=oidc` endpoint. + // + // required: false + // in: query + Identifier string `json:"identifier"` } // swagger:route DELETE /admin/identities/{id}/credentials/{type} identity deleteIdentityCredentials // // # Delete a credential for a specific identity // -// Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type -// You can only delete second factor (aal2) credentials. +// Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type. +// You cannot delete password or code auth credentials through this API. // // Consumes: // - application/json @@ -1033,14 +1050,18 @@ func (h *Handler) deleteIdentityCredentials(w http.ResponseWriter, r *http.Reque case CredentialsTypeLookup, CredentialsTypeTOTP: identity.DeleteCredentialsType(cred.Type) case CredentialsTypeWebAuthn: - identity, err = deletCredentialWebAuthFromIdentity(identity) - if err != nil { + if err = identity.deleteCredentialWebAuthFromIdentity(); err != nil { h.r.Writer().WriteError(w, r, err) return } - case CredentialsTypeOIDC, CredentialsTypePassword, CredentialsTypeCodeAuth: - h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("You can't remove first factor credentials."))) + case CredentialsTypePassword, CredentialsTypeCodeAuth: + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("You cannot remove first factor credentials."))) return + case CredentialsTypeOIDC: + if err := identity.deleteCredentialOIDCFromIdentity(r.URL.Query().Get("identifier")); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } default: h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unknown credentials type %s.", cred.Type))) return diff --git a/identity/handler_import.go b/identity/handler_import.go index a3a7ca2ab0a1..babb09579af1 100644 --- a/identity/handler_import.go +++ b/identity/handler_import.go @@ -19,7 +19,15 @@ func (h *Handler) importCredentials(ctx context.Context, i *Identity, creds *Ide return nil } + // This method only support password and OIDC import at the moment. + // If other methods are added please ensure that the available AAL is set correctly in the identity. + // + // It would actually be good if we would validate the identity post-creation to see if the credentials are working. if creds.Password != nil { + // This method is somewhat hacky, because it does not set the credential's identifier. It relies on the + // identity validation to set the identifier, which is called after this method. + // + // It would be good to make this explicit. if err := h.importPasswordCredentials(ctx, i, creds.Password); err != nil { return err } @@ -35,6 +43,10 @@ func (h *Handler) importCredentials(ctx context.Context, i *Identity, creds *Ide } func (h *Handler) importPasswordCredentials(ctx context.Context, i *Identity, creds *AdminIdentityImportCredentialsPassword) (err error) { + if creds.Config.UsePasswordMigrationHook { + return i.SetCredentialsWithConfig(CredentialsTypePassword, Credentials{}, CredentialsPassword{UsePasswordMigrationHook: true}) + } + // In here we deliberately ignore any password policies as the point here is to import passwords, even if they // are not matching the policy, as the user needs to able to sign in with their old password. hashed := []byte(creds.Config.HashedPassword) diff --git a/identity/handler_test.go b/identity/handler_test.go index 41df0d3674b9..4af2c76b9d8c 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -35,6 +35,8 @@ import ( "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/strategy/totp" "github.com/ory/kratos/x" + "github.com/ory/x/ioutilx" + "github.com/ory/x/randx" "github.com/ory/x/snapshotx" "github.com/ory/x/sqlxx" "github.com/ory/x/urlx" @@ -84,8 +86,9 @@ func TestHandler(t *testing.T) { res, err := base.Client().Do(req) require.NoError(t, err) + defer res.Body.Close() - require.EqualValues(t, expectCode, res.StatusCode) + require.EqualValues(t, expectCode, res.StatusCode, "%s", ioutilx.MustReadAll(res.Body)) } send := func(t *testing.T, base *httptest.Server, method, href string, expectCode int, send interface{}) gjson.Result { @@ -341,6 +344,21 @@ func TestHandler(t *testing.T) { } }) + t.Run("with password migration hook enabled", func(t *testing.T) { + res := send(t, adminTS, "POST", "/identities", http.StatusCreated, identity.CreateIdentityBody{ + Traits: []byte(`{"email": "pw-migration-hook@ory.sh"}`), + Credentials: &identity.IdentityWithCredentials{Password: &identity.AdminIdentityImportCredentialsPassword{ + Config: identity.AdminIdentityImportCredentialsPasswordConfig{UsePasswordMigrationHook: true}, + }}, + }) + actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, uuid.FromStringOrNil(res.Get("id").String())) + require.NoError(t, err) + + snapshotx.SnapshotT(t, identity.WithCredentialsAndAdminMetadataInJSON(*actual), snapshotx.ExceptNestedKeys(ignoreDefault...), snapshotx.ExceptNestedKeys("hashed_password")) + + assert.True(t, gjson.GetBytes(actual.Credentials[identity.CredentialsTypePassword].Config, "use_password_migration_hook").Bool()) + }) + t.Run("with not-normalized email", func(t *testing.T) { res := send(t, adminTS, "POST", "/identities", http.StatusCreated, identity.CreateIdentityBody{ SchemaID: "customer", @@ -391,27 +409,34 @@ func TestHandler(t *testing.T) { require.Equal(t, len(ids), identitiesAmount) }) - t.Run("case= list few identities", func(t *testing.T) { + t.Run("case=list few identities", func(t *testing.T) { url := "/identities?ids=" + ids[0].String() for i := 1; i < listAmount; i++ { url += "&ids=" + ids[i].String() } - res := get(t, adminTS, url, 200) + res := get(t, adminTS, url, http.StatusOK) identities := res.Array() require.Equal(t, len(identities), listAmount) }) + }) + t.Run("case=malformed ids should return an error", func(t *testing.T) { + res := get(t, adminTS, "/identities?ids=not-a-uuid", http.StatusBadRequest) + assert.Contains(t, res.Get("error.reason").String(), "Invalid UUID value `not-a-uuid` for parameter `ids`.", "%s", res.Raw) }) t.Run("suite=create and update", func(t *testing.T) { var i identity.Identity createOidcIdentity := func(t *testing.T, identifier, accessToken, refreshToken, idToken string, encrypt bool) string { - transform := func(token string) string { + transform := func(token, suffix string) string { if !encrypt { return token } - c, err := reg.Cipher(ctx).Encrypt(context.Background(), []byte(token)) + if token == "" { + return "" + } + c, err := reg.Cipher(ctx).Encrypt(context.Background(), []byte(token+suffix)) require.NoError(t, err) return c } @@ -433,16 +458,16 @@ func TestHandler(t *testing.T) { { Subject: "foo", Provider: "bar", - InitialAccessToken: transform(accessToken + "0"), - InitialRefreshToken: transform(refreshToken + "0"), - InitialIDToken: transform(idToken + "0"), + InitialAccessToken: transform(accessToken, "0"), + InitialRefreshToken: transform(refreshToken, "0"), + InitialIDToken: transform(idToken, "0"), }, { Subject: "baz", Provider: "zab", - InitialAccessToken: transform(accessToken + "1"), - InitialRefreshToken: transform(refreshToken + "1"), - InitialIDToken: transform(idToken + "1"), + InitialAccessToken: transform(accessToken, "1"), + InitialRefreshToken: transform(refreshToken, "1"), + InitialIDToken: transform(idToken, "1"), }, }}), }, @@ -574,6 +599,34 @@ func TestHandler(t *testing.T) { } }) + t.Run("case=should not fail on empty tokens", func(t *testing.T) { + id := createOidcIdentity(t, "foo.oidc.empty-tokens@bar.com", "", "", "", true) + for name, ts := range map[string]*httptest.Server{"public": publicTS, "admin": adminTS} { + t.Run("endpoint="+name, func(t *testing.T) { + res := get(t, ts, "/identities/"+id, http.StatusOK) + assert.False(t, res.Get("credentials.oidc.config").Exists(), "credentials config should be omitted: %s", res.Raw) + assert.False(t, res.Get("credentials.password.config").Exists(), "credentials config should be omitted: %s", res.Raw) + + res = get(t, ts, "/identities/"+id+"?include_credential=oidc", http.StatusOK) + assert.True(t, res.Get("credentials").Exists(), "credentials should be included: %s", res.Raw) + assert.True(t, res.Get("credentials.password").Exists(), "password meta should be included: %s", res.Raw) + assert.False(t, res.Get("credentials.password.false").Exists(), "password credentials should not be included: %s", res.Raw) + assert.True(t, res.Get("credentials.oidc.config").Exists(), "oidc credentials should be included: %s", res.Raw) + + assert.EqualValues(t, "foo", res.Get("credentials.oidc.config.providers.0.subject").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "bar", res.Get("credentials.oidc.config.providers.0.provider").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.0.initial_access_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.0.initial_refresh_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.0.initial_id_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "baz", res.Get("credentials.oidc.config.providers.1.subject").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "zab", res.Get("credentials.oidc.config.providers.1.provider").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.1.initial_access_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.1.initial_refresh_token").String(), "credentials should be included: %s", res.Raw) + assert.EqualValues(t, "", res.Get("credentials.oidc.config.providers.1.initial_id_token").String(), "credentials should be included: %s", res.Raw) + }) + } + }) + t.Run("case=should get identity with credentials", func(t *testing.T) { i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) credentials := map[identity.CredentialsType]identity.Credentials{ @@ -617,22 +670,25 @@ func TestHandler(t *testing.T) { } }) - t.Run("case=should fail to get oidc credential", func(t *testing.T) { + t.Run("case=should return empty tokens if decryption fails", func(t *testing.T) { id := createOidcIdentity(t, "foo-failed.oidc@bar.com", "foo_token", "bar_token", "id_token", false) for name, ts := range map[string]*httptest.Server{"public": publicTS, "admin": adminTS} { t.Run("endpoint="+name, func(t *testing.T) { - t.Logf("no oidc token") res := get(t, ts, "/identities/"+i.ID.String()+"?include_credential=oidc", http.StatusOK) assert.NotContains(t, res.Raw, "identifier_credentials", res.Raw) - t.Logf("get oidc token") - res = get(t, ts, "/identities/"+id+"?include_credential=oidc", http.StatusInternalServerError) - assert.Contains(t, res.Raw, "Internal Server Error", res.Raw) + res = get(t, ts, "/identities/"+id+"?include_credential=oidc", http.StatusOK) + assert.Equal(t, "bar:foo-failed.oidc@bar.com", res.Get("credentials.oidc.identifiers.0").String(), "%s", res.Raw) + assert.Equal(t, "", res.Get("credentials.oidc.config.providers.0.initial_access_token").String(), "%s", res.Raw) + assert.Equal(t, "", res.Get("credentials.oidc.config.providers.0.initial_id_token").String(), "%s", res.Raw) + assert.Equal(t, "", res.Get("credentials.oidc.config.providers.0.initial_refresh_token").String(), "%s", res.Raw) }) } + }) + t.Run("case=should return decrypted token", func(t *testing.T) { e, _ := reg.Cipher(ctx).Encrypt(context.Background(), []byte("foo_token")) - id = createOidcIdentity(t, "foo-failed-2.oidc@bar.com", e, "bar_token", "id_token", false) + id := createOidcIdentity(t, "foo-failed-2.oidc@bar.com", e, "bar_token", "id_token", false) for name, ts := range map[string]*httptest.Server{"public": publicTS, "admin": adminTS} { t.Run("endpoint="+name, func(t *testing.T) { t.Logf("no oidc token") @@ -640,8 +696,9 @@ func TestHandler(t *testing.T) { assert.NotContains(t, res.Raw, "identifier_credentials", res.Raw) t.Logf("get oidc token") - res = get(t, ts, "/identities/"+id+"?include_credential=oidc", http.StatusInternalServerError) - assert.Contains(t, res.Raw, "Internal Server Error", res.Raw) + res = get(t, ts, "/identities/"+id+"?include_credential=oidc", http.StatusOK) + assert.Equal(t, "bar:foo-failed-2.oidc@bar.com", res.Get("credentials.oidc.identifiers.0").String(), "%s", res.Raw) + assert.Equal(t, "foo_token", res.Get("credentials.oidc.config.providers.0.initial_access_token").String(), "%s", res.Raw) }) } }) @@ -760,53 +817,63 @@ func TestHandler(t *testing.T) { assert.Contains(t, res.Get("error.reason").String(), strconv.Itoa(identity.BatchPatchIdentitiesLimit), "the error reason should contain the limit") }) - t.Run("case=fails all on a bad identity", func(t *testing.T) { + t.Run("case=fails some on a bad identity", func(t *testing.T) { // Test setup: we have a list of valid identitiy patches and a list of invalid ones. // Each run adds one invalid patch to the list and sends it to the server. // --> we expect the server to fail all patches in the list. // Finally, we send just the valid patches // --> we expect the server to succeed all patches in the list. - validPatches := []*identity.BatchIdentityPatch{ - {Create: validCreateIdentityBody("valid-patch", 0)}, - {Create: validCreateIdentityBody("valid-patch", 1)}, - {Create: validCreateIdentityBody("valid-patch", 2)}, - {Create: validCreateIdentityBody("valid-patch", 3)}, - {Create: validCreateIdentityBody("valid-patch", 4)}, - } - for _, tt := range []struct { - name string - body *identity.CreateIdentityBody - expectStatus int - }{ - { - name: "missing all fields", - body: &identity.CreateIdentityBody{}, - expectStatus: http.StatusBadRequest, - }, - { - name: "duplicate identity", - body: validCreateIdentityBody("valid-patch", 0), - expectStatus: http.StatusConflict, - }, - { - name: "invalid traits", - body: &identity.CreateIdentityBody{ - Traits: json.RawMessage(`"invalid traits"`), - }, - expectStatus: http.StatusBadRequest, - }, - } { - t.Run("invalid because "+tt.name, func(t *testing.T) { - patches := append([]*identity.BatchIdentityPatch{}, validPatches...) - patches = append(patches, &identity.BatchIdentityPatch{Create: tt.body}) + t.Run("case=invalid patches fail", func(t *testing.T) { + patches := []*identity.BatchIdentityPatch{ + {Create: validCreateIdentityBody("valid", 0)}, + {Create: validCreateIdentityBody("valid", 1)}, + {Create: &identity.CreateIdentityBody{}}, // <-- invalid: missing all fields + {Create: validCreateIdentityBody("valid", 2)}, + {Create: validCreateIdentityBody("valid", 0)}, // <-- duplicate + {Create: validCreateIdentityBody("valid", 3)}, + {Create: &identity.CreateIdentityBody{Traits: json.RawMessage(`"invalid traits"`)}}, // <-- invalid traits + {Create: validCreateIdentityBody("valid", 4)}, + } - req := &identity.BatchPatchIdentitiesBody{Identities: patches} - send(t, adminTS, "PATCH", "/identities", tt.expectStatus, req) - }) - } + // Create unique IDs for each patch + var patchIDs []string + for i, p := range patches { + id := uuid.NewV5(uuid.Nil, fmt.Sprintf("%d", i)) + p.ID = &id + patchIDs = append(patchIDs, id.String()) + } + + req := &identity.BatchPatchIdentitiesBody{Identities: patches} + body := send(t, adminTS, "PATCH", "/identities", http.StatusOK, req) + var actions []string + for _, a := range body.Get("identities.#.action").Array() { + actions = append(actions, a.String()) + } + assert.Equal(t, + []string{"create", "create", "error", "create", "error", "create", "error", "create"}, + actions, body) + + // Check that all patch IDs are returned + for i, gotPatchID := range body.Get("identities.#.patch_id").Array() { + assert.Equal(t, patchIDs[i], gotPatchID.String()) + } + + // Check specific errors + assert.Equal(t, "Bad Request", body.Get("identities.2.error.status").String()) + assert.Equal(t, "Conflict", body.Get("identities.4.error.status").String()) + assert.Equal(t, "Bad Request", body.Get("identities.6.error.status").String()) + + }) t.Run("valid patches succeed", func(t *testing.T) { + validPatches := []*identity.BatchIdentityPatch{ + {Create: validCreateIdentityBody("valid-patch", 0)}, + {Create: validCreateIdentityBody("valid-patch", 1)}, + {Create: validCreateIdentityBody("valid-patch", 2)}, + {Create: validCreateIdentityBody("valid-patch", 3)}, + {Create: validCreateIdentityBody("valid-patch", 4)}, + } req := &identity.BatchPatchIdentitiesBody{Identities: validPatches} send(t, adminTS, "PATCH", "/identities", http.StatusOK, req) }) @@ -947,6 +1014,98 @@ func TestHandler(t *testing.T) { } }) + t.Run("case=PATCH should update verified_at timestamp", func(t *testing.T) { + for name, ts := range map[string]*httptest.Server{"public": publicTS, "admin": adminTS} { + t.Run("endpoint="+name, func(t *testing.T) { + email := x.NewUUID().String() + "@ory.sh" + var cr identity.CreateIdentityBody + cr.SchemaID = "employee" + cr.Traits = []byte(`{"email":"` + email + `"}`) + res := send(t, ts, "POST", "/identities", http.StatusCreated, &cr) + assert.EqualValues(t, email, res.Get("recovery_addresses.0.value").String(), "%s", res.Raw) + assert.EqualValues(t, email, res.Get("verifiable_addresses.0.value").String(), "%s", res.Raw) + assert.Falsef(t, res.Get("verifiable_addresses.0.verified").Bool(), "%s", res.Raw) + assert.Falsef(t, res.Get("verifiable_addresses.0.verified_at").Exists(), "%s", res.Raw) + identityID := res.Get("id").String() + + // set to verified, should also update verified_at timestamp + patch1 := []patch{ + { + "op": "replace", + "path": "/verifiable_addresses/0/verified", + "value": true, + }, + } + + now := time.Now() + + res = send(t, ts, "PATCH", "/identities/"+identityID, http.StatusOK, &patch1) + assert.EqualValues(t, email, res.Get("recovery_addresses.0.value").String(), "%s", res.Raw) + assert.EqualValues(t, email, res.Get("verifiable_addresses.0.value").String(), "%s", res.Raw) + assert.Truef(t, res.Get("verifiable_addresses.0.verified").Bool(), "%s", res.Raw) + assert.WithinDurationf(t, now, res.Get("verifiable_addresses.0.updated_at").Time(), 5*time.Second, "%s", res.Raw) + assert.WithinDurationf(t, now, res.Get("verifiable_addresses.0.verified_at").Time(), 5*time.Second, "%s", res.Raw) + + res = get(t, ts, "/identities/"+identityID, http.StatusOK) + assert.EqualValues(t, email, res.Get("recovery_addresses.0.value").String(), "%s", res.Raw) + assert.EqualValues(t, email, res.Get("verifiable_addresses.0.value").String(), "%s", res.Raw) + assert.Truef(t, res.Get("verifiable_addresses.0.verified").Bool(), "%s", res.Raw) + assert.WithinDurationf(t, now, res.Get("verifiable_addresses.0.updated_at").Time(), 5*time.Second, "%s", res.Raw) + assert.WithinDurationf(t, now, res.Get("verifiable_addresses.0.verified_at").Time(), 5*time.Second, "%s", res.Raw) + + // update only verified_at timestamp + verifiedAt := time.Date(1999, 1, 7, 8, 23, 19, 0, time.UTC) + patch2 := []patch{ + { + "op": "replace", + "path": "/verifiable_addresses/0/verified_at", + "value": verifiedAt.Format(time.RFC3339), + }, + } + + now = time.Now() + res = send(t, ts, "PATCH", "/identities/"+identityID, http.StatusOK, &patch2) + assert.EqualValues(t, email, res.Get("recovery_addresses.0.value").String(), "%s", res.Raw) + assert.EqualValues(t, email, res.Get("verifiable_addresses.0.value").String(), "%s", res.Raw) + assert.Truef(t, res.Get("verifiable_addresses.0.verified").Bool(), "%s", res.Raw) + assert.Equalf(t, verifiedAt, res.Get("verifiable_addresses.0.verified_at").Time(), "%s", res.Raw) + assert.WithinDurationf(t, now, res.Get("verifiable_addresses.0.updated_at").Time(), 5*time.Second, "%s", res.Raw) + + res = get(t, ts, "/identities/"+identityID, http.StatusOK) + assert.EqualValues(t, email, res.Get("recovery_addresses.0.value").String(), "%s", res.Raw) + assert.EqualValues(t, email, res.Get("verifiable_addresses.0.value").String(), "%s", res.Raw) + assert.Truef(t, res.Get("verifiable_addresses.0.verified").Bool(), "%s", res.Raw) + assert.Equalf(t, verifiedAt, res.Get("verifiable_addresses.0.verified_at").Time(), "%s", res.Raw) + assert.WithinDurationf(t, now, res.Get("verifiable_addresses.0.updated_at").Time(), 5*time.Second, "%s", res.Raw) + + // remove verified status + patch3 := []patch{ + { + "op": "replace", + "path": "/verifiable_addresses/0/verified", + "value": false, + }, + } + + now = time.Now() + + res = send(t, ts, "PATCH", "/identities/"+identityID, http.StatusOK, &patch3) + assert.EqualValues(t, email, res.Get("recovery_addresses.0.value").String(), "%s", res.Raw) + assert.EqualValues(t, email, res.Get("verifiable_addresses.0.value").String(), "%s", res.Raw) + assert.Falsef(t, res.Get("verifiable_addresses.0.verified").Bool(), "%s", res.Raw) + assert.Falsef(t, res.Get("verifiable_addresses.0.verified_at").Exists(), "%s", res.Raw) + assert.WithinDurationf(t, now, res.Get("verifiable_addresses.0.updated_at").Time(), 5*time.Second, "%s", res.Raw) + + res = get(t, ts, "/identities/"+identityID, http.StatusOK) + assert.EqualValues(t, email, res.Get("recovery_addresses.0.value").String(), "%s", res.Raw) + assert.EqualValues(t, email, res.Get("verifiable_addresses.0.value").String(), "%s", res.Raw) + assert.Falsef(t, res.Get("verifiable_addresses.0.verified").Bool(), "%s", res.Raw) + assert.Falsef(t, res.Get("verifiable_addresses.0.verified_at").Exists(), "%s", res.Raw) + assert.WithinDurationf(t, now, res.Get("verifiable_addresses.0.updated_at").Time(), 5*time.Second, "%s", res.Raw) + }) + } + }) + t.Run("case=PATCH update should not persist if schema id is invalid", func(t *testing.T) { uuid := x.NewUUID().String() i := &identity.Identity{Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, uuid))} @@ -1355,7 +1514,7 @@ func TestHandler(t *testing.T) { }) t.Run("case=should list all identities", func(t *testing.T) { - for name, ts := range map[string]*httptest.Server{"public": publicTS, "admin": adminTS} { + for name, ts := range map[string]*httptest.Server{"admin": adminTS} { t.Run("endpoint="+name, func(t *testing.T) { res := get(t, ts, "/identities", http.StatusOK) assert.False(t, res.Get("0.credentials").Exists(), "credentials config should be omitted: %s", res.Raw) @@ -1366,6 +1525,27 @@ func TestHandler(t *testing.T) { } }) + t.Run("case=should list all identities with credentials", func(t *testing.T) { + t.Run("include_credential=oidc should include OIDC credentials config", func(t *testing.T) { + res := get(t, adminTS, "/identities?include_credential=oidc&credentials_identifier=bar:foo.oidc@bar.com", http.StatusOK) + assert.True(t, res.Get("0.credentials.oidc.config").Exists(), "credentials config should be included: %s", res.Raw) + snapshotx.SnapshotT(t, res.Get("0.credentials.oidc.config").String()) + }) + t.Run("include_credential=totp should not include OIDC credentials config", func(t *testing.T) { + res := get(t, adminTS, "/identities?include_credential=totp&credentials_identifier=bar:foo.oidc@bar.com", http.StatusOK) + assert.False(t, res.Get("0.credentials.oidc.config").Exists(), "credentials config should be included: %s", res.Raw) + }) + }) + + t.Run("case=should not be able to list all identities with credentials due to wrong credentials type", func(t *testing.T) { + for name, ts := range map[string]*httptest.Server{"admin": adminTS} { + t.Run("endpoint="+name, func(t *testing.T) { + res := get(t, ts, "/identities?include_credential=XYZ", http.StatusBadRequest) + assert.Contains(t, res.Get("error.message").String(), "The request was malformed or contained invalid parameters", "%s", res.Raw) + }) + } + }) + t.Run("case=should list all identities with eventual consistency", func(t *testing.T) { for name, ts := range map[string]*httptest.Server{"public": publicTS, "admin": adminTS} { t.Run("endpoint="+name, func(t *testing.T) { @@ -1403,15 +1583,15 @@ func TestHandler(t *testing.T) { t.Run("case=should delete credential of a specific user and no longer be able to retrieve it", func(t *testing.T) { ignoreDefault := []string{"id", "schema_url", "state_changed_at", "created_at", "updated_at"} - createIdentity := func(identities map[identity.CredentialsType]string) func(t *testing.T) *identity.Identity { + type M = map[identity.CredentialsType]identity.Credentials + createIdentity := func(creds M) func(*testing.T) *identity.Identity { return func(t *testing.T) *identity.Identity { i := identity.NewIdentity("") - for ct, config := range identities { - i.SetCredentials(ct, identity.Credentials{ - Type: ct, - Config: sqlxx.JSONRawMessage(config), - }) + for k, v := range creds { + v.Type = k + creds[k] = v } + i.Credentials = creds i.Traits = identity.Traits("{}") require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i)) return i @@ -1422,26 +1602,83 @@ func TestHandler(t *testing.T) { remove(t, ts, "/identities/"+x.NewUUID().String()+"/credentials/azerty", http.StatusNotFound) }) t.Run("type=remove unknown type/"+name, func(t *testing.T) { - i := createIdentity(map[identity.CredentialsType]string{ - identity.CredentialsTypePassword: `{"secret":"pst"}`, + i := createIdentity(M{ + identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)}, })(t) remove(t, ts, "/identities/"+i.ID.String()+"/credentials/azerty", http.StatusNotFound) }) t.Run("type=remove password type/"+name, func(t *testing.T) { - i := createIdentity(map[identity.CredentialsType]string{ - identity.CredentialsTypePassword: `{"secret":"pst"}`, + i := createIdentity(M{ + identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)}, })(t) remove(t, ts, "/identities/"+i.ID.String()+"/credentials/password", http.StatusBadRequest) }) t.Run("type=remove oidc type/"+name, func(t *testing.T) { - i := createIdentity(map[identity.CredentialsType]string{ - identity.CredentialsTypeOIDC: `{"id":"pst"}`, + // force ordering among github identifiers + githubSubject := "0" + randx.MustString(7, randx.Numeric) + githubSubject2 := "1" + randx.MustString(7, randx.Numeric) + googleSubject := randx.MustString(8, randx.Numeric) + initialConfig := []byte(fmt.Sprintf(`{ + "providers": [ + { + "subject": %q, + "provider": "github" + }, + { + "subject": %q, + "provider": "github" + }, + { + "subject": %q, + "provider": "google" + } + ] + }`, githubSubject, githubSubject2, googleSubject)) + identifiers := []string{ + identity.OIDCUniqueID("github", githubSubject), + identity.OIDCUniqueID("github", githubSubject2), + identity.OIDCUniqueID("google", googleSubject), + } + i := createIdentity(M{ + identity.CredentialsTypeOIDC: { + Identifiers: identifiers, + Config: initialConfig, + }, })(t) - remove(t, ts, "/identities/"+i.ID.String()+"/credentials/oidc", http.StatusBadRequest) + res := get(t, ts, "/identities/"+i.ID.String()+"?include_credential=oidc", http.StatusOK) + assert.EqualValues(t, i.ID.String(), res.Get("id").String(), "%s", res.Raw) + assert.Len(t, res.Get("credentials.oidc.identifiers").Array(), 3, "%s", res.Raw) + assert.EqualValues(t, res.Get("credentials.oidc.identifiers.0").String(), identifiers[0], "%s", res.Raw) + assert.EqualValues(t, res.Get("credentials.oidc.identifiers.1").String(), identifiers[1], "%s", res.Raw) + assert.EqualValues(t, res.Get("credentials.oidc.identifiers.2").String(), identifiers[2], "%s", res.Raw) + + oidConfig := gjson.Parse(res.Get("credentials.oidc.config").String()) + assert.Len(t, res.Get("credentials.oidc.identifiers").Array(), 3, "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.0.provider").String(), "github", "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.0.subject").String(), githubSubject, "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.1.provider").String(), "github", "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.1.subject").String(), githubSubject2, "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.2.provider").String(), "google", "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.2.subject").String(), googleSubject, "%s", res.Raw) + + remove(t, ts, "/identities/"+i.ID.String()+"/credentials/oidc?identifier="+identifiers[1], http.StatusNoContent) + res = get(t, ts, "/identities/"+i.ID.String()+"?include_credential=oidc", http.StatusOK) + + assert.EqualValues(t, i.ID.String(), res.Get("id").String(), "%s", res.Raw) + assert.Len(t, res.Get("credentials.oidc.identifiers").Array(), 2, "%s", res.Raw) + assert.EqualValues(t, res.Get("credentials.oidc.identifiers.0").String(), identifiers[0], "%s", res.Raw) + assert.EqualValues(t, res.Get("credentials.oidc.identifiers.1").String(), identifiers[2], "%s", res.Raw) + + oidConfig = gjson.Parse(res.Get("credentials.oidc.config").String()) + assert.Len(t, res.Get("credentials.oidc.identifiers").Array(), 2, "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.0.provider").String(), "github", "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.0.subject").String(), githubSubject, "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.1.provider").String(), "google", "%s", res.Raw) + assert.EqualValues(t, oidConfig.Get("providers.1.subject").String(), googleSubject, "%s", res.Raw) }) t.Run("type=remove webauthn passwordless type/"+name, func(t *testing.T) { expected := `{"credentials":[{"id":"THTndqZP5Mjvae1BFvJMaMfEMm7O7HE1ju+7PBaYA7Y=","added_at":"2022-12-16T14:11:55Z","public_key":"pQECAyYgASFYIMJLQhJxQRzhnKPTcPCUODOmxYDYo2obrm9bhp5lvSZ3IlggXjhZvJaPUqF9PXqZqTdWYPR7R+b2n/Wi+IxKKXsS4rU=","display_name":"test","authenticator":{"aaguid":"rc4AAjW8xgpkiwsl8fBVAw==","sign_count":0,"clone_warning":false},"is_passwordless":true,"attestation_type":"none"}],"user_handle":"Ef5JiMpMRwuzauWs/9J0gQ=="}` - i := createIdentity(map[identity.CredentialsType]string{identity.CredentialsTypeWebAuthn: expected})(t) + i := createIdentity(M{identity.CredentialsTypeWebAuthn: {Config: []byte(expected)}})(t) remove(t, ts, "/identities/"+i.ID.String()+"/credentials/webauthn", http.StatusNoContent) // Check that webauthn has not been deleted res := get(t, ts, "/identities/"+i.ID.String(), http.StatusOK) @@ -1460,7 +1697,7 @@ func TestHandler(t *testing.T) { AddedAt: time.Date(2022, 12, 16, 14, 11, 55, 0, time.UTC), PublicKey: []byte("pQECAyYgASFYIMJLQhJxQRzhnKPTcPCUODOmxYDYo2obrm9bhp5lvSZ3IlggXjhZvJaPUqF9PXqZqTdWYPR7R+b2n/Wi+IxKKXsS4rU="), DisplayName: "test", - Authenticator: identity.AuthenticatorWebAuthn{ + Authenticator: &identity.AuthenticatorWebAuthn{ AAGUID: []byte("rc4AAjW8xgpkiwsl8fBVAw=="), SignCount: 0, CloneWarning: false, @@ -1473,7 +1710,7 @@ func TestHandler(t *testing.T) { AddedAt: time.Date(2022, 12, 16, 14, 11, 55, 0, time.UTC), PublicKey: []byte("pQECAyYgASFYIMJLQhJxQRzhnKPTcPCUODOmxYDYo2obrm9bhp5lvSZ3IlggXjhZvJaPUqF9PXqZqTdWYPR7R+b2n/Wi+IxKKXsS4rU="), DisplayName: "test", - Authenticator: identity.AuthenticatorWebAuthn{ + Authenticator: &identity.AuthenticatorWebAuthn{ AAGUID: []byte("rc4AAjW8xgpkiwsl8fBVAw=="), SignCount: 0, CloneWarning: false, @@ -1486,7 +1723,7 @@ func TestHandler(t *testing.T) { AddedAt: time.Date(2022, 12, 16, 14, 11, 55, 0, time.UTC), PublicKey: []byte("pQECAyYgASFYIMJLQhJxQRzhnKPTcPCUODOmxYDYo2obrm9bhp5lvSZ3IlggXjhZvJaPUqF9PXqZqTdWYPR7R+b2n/Wi+IxKKXsS4rU="), DisplayName: "test", - Authenticator: identity.AuthenticatorWebAuthn{ + Authenticator: &identity.AuthenticatorWebAuthn{ AAGUID: []byte("rc4AAjW8xgpkiwsl8fBVAw=="), SignCount: 0, CloneWarning: false, @@ -1499,7 +1736,7 @@ func TestHandler(t *testing.T) { AddedAt: time.Date(2022, 12, 16, 14, 11, 55, 0, time.UTC), PublicKey: []byte("pQECAyYgASFYIMJLQhJxQRzhnKPTcPCUODOmxYDYo2obrm9bhp5lvSZ3IlggXjhZvJaPUqF9PXqZqTdWYPR7R+b2n/Wi+IxKKXsS4rU="), DisplayName: "test", - Authenticator: identity.AuthenticatorWebAuthn{ + Authenticator: &identity.AuthenticatorWebAuthn{ AAGUID: []byte("rc4AAjW8xgpkiwsl8fBVAw=="), SignCount: 0, CloneWarning: false, @@ -1514,7 +1751,7 @@ func TestHandler(t *testing.T) { message, err := json.Marshal(config) require.NoError(t, err) - i := createIdentity(map[identity.CredentialsType]string{identity.CredentialsTypeWebAuthn: string(message)})(t) + i := createIdentity(M{identity.CredentialsTypeWebAuthn: {Config: message}})(t) remove(t, ts, "/identities/"+i.ID.String()+"/credentials/webauthn", http.StatusNoContent) // Check that webauthn has not been deleted res := get(t, ts, "/identities/"+i.ID.String(), http.StatusOK) @@ -1524,10 +1761,10 @@ func TestHandler(t *testing.T) { require.NoError(t, err) snapshotx.SnapshotT(t, identity.WithCredentialsAndAdminMetadataInJSON(*actual), snapshotx.ExceptNestedKeys(append(ignoreDefault, "hashed_password")...), snapshotx.ExceptPaths("credentials.oidc.identifiers")) }) - for ct, ctConf := range map[identity.CredentialsType]string{ - identity.CredentialsTypeLookup: `{"recovery_codes": [{"code": "aaa"}]}`, - identity.CredentialsTypeTOTP: `{"totp_url":"otpauth://totp/test"}`, - identity.CredentialsTypeWebAuthn: `{"credentials":[{"id":"THTndqZP5Mjvae1BFvJMaMfEMm7O7HE1ju+7PBaYA7Y=","added_at":"2022-12-16T14:11:55Z","public_key":"pQECAyYgASFYIMJLQhJxQRzhnKPTcPCUODOmxYDYo2obrm9bhp5lvSZ3IlggXjhZvJaPUqF9PXqZqTdWYPR7R+b2n/Wi+IxKKXsS4rU=","display_name":"test","authenticator":{"aaguid":"rc4AAjW8xgpkiwsl8fBVAw==","sign_count":0,"clone_warning":false},"is_passwordless":false,"attestation_type":"none"}],"user_handle":"Ef5JiMpMRwuzauWs/9J0gQ=="}`, + for ct, ctConf := range map[identity.CredentialsType][]byte{ + identity.CredentialsTypeLookup: []byte(`{"recovery_codes": [{"code": "aaa"}]}`), + identity.CredentialsTypeTOTP: []byte(`{"totp_url":"otpauth://totp/test"}`), + identity.CredentialsTypeWebAuthn: []byte(`{"credentials":[{"id":"THTndqZP5Mjvae1BFvJMaMfEMm7O7HE1ju+7PBaYA7Y=","added_at":"2022-12-16T14:11:55Z","public_key":"pQECAyYgASFYIMJLQhJxQRzhnKPTcPCUODOmxYDYo2obrm9bhp5lvSZ3IlggXjhZvJaPUqF9PXqZqTdWYPR7R+b2n/Wi+IxKKXsS4rU=","display_name":"test","authenticator":{"aaguid":"rc4AAjW8xgpkiwsl8fBVAw==","sign_count":0,"clone_warning":false},"is_passwordless":false,"attestation_type":"none"}],"user_handle":"Ef5JiMpMRwuzauWs/9J0gQ=="}`), } { t.Run("type=remove "+string(ct)+"/"+name, func(t *testing.T) { for _, tc := range []struct { @@ -1538,25 +1775,25 @@ func TestHandler(t *testing.T) { { desc: "with", exist: true, - setup: createIdentity(map[identity.CredentialsType]string{ - identity.CredentialsTypePassword: `{"secret":"pst"}`, - ct: ctConf, + setup: createIdentity(M{ + identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)}, + ct: {Config: ctConf}, }), }, { desc: "without", exist: false, - setup: createIdentity(map[identity.CredentialsType]string{ - identity.CredentialsTypePassword: `{"secret":"pst"}`, + setup: createIdentity(M{ + identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)}, }), }, { desc: "multiple", exist: true, - setup: createIdentity(map[identity.CredentialsType]string{ - identity.CredentialsTypePassword: `{"secret":"pst"}`, - identity.CredentialsTypeOIDC: `{"id":"pst"}`, - ct: ctConf, + setup: createIdentity(M{ + identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)}, + identity.CredentialsTypeOIDC: {Config: []byte(`{"id":"pst"}`)}, + ct: {Config: ctConf}, }), }, } { diff --git a/identity/identity.go b/identity/identity.go index 1a2bb5a0b012..14b68dce9020 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -11,21 +11,18 @@ import ( "sync" "time" + "github.com/gofrs/uuid" + "github.com/pkg/errors" "github.com/samber/lo" - - "github.com/tidwall/sjson" - "github.com/tidwall/gjson" - - "github.com/ory/kratos/cipher" + "github.com/tidwall/sjson" "github.com/ory/herodot" + "github.com/ory/kratos/cipher" + "github.com/ory/kratos/driver/config" "github.com/ory/x/pagination/keysetpagination" "github.com/ory/x/sqlxx" - "github.com/ory/kratos/driver/config" - "github.com/ory/kratos/x" - "github.com/gofrs/uuid" "github.com/pkg/errors" ) @@ -69,9 +66,17 @@ type Identity struct { // Credentials represents all credentials that can be used for authenticating this identity. Credentials map[CredentialsType]Credentials `json:"credentials,omitempty" faker:"-" db:"-"` - // AvailableAAL defines the maximum available AAL for this identity. If the user has only a password - // configured, the AAL will be 1. If the user has a password and a TOTP configured, the AAL will be 2. - AvailableAAL NullableAuthenticatorAssuranceLevel `json:"-" faker:"-" db:"available_aal"` + // InternalAvailableAAL defines the maximum available AAL for this identity. + // + // - If the user has at least one two-factor authentication method configured, the AAL will be 2. + // - If the user has only a password configured, the AAL will be 1. + // + // This field is AAL2 as soon as a second factor credential is found. A first factor is not required for this + // field to return `aal2`. + // + // This field is primarily used to determine whether the user needs to upgrade to AAL2 without having to check + // all the credentials in the database. Use with caution! + InternalAvailableAAL NullableAuthenticatorAssuranceLevel `json:"-" faker:"-" db:"available_aal"` // // IdentifierCredentials contains the access and refresh token for oidc identifier // IdentifierCredentials []IdentifierCredential `json:"identifier_credentials,omitempty" faker:"-" db:"-"` @@ -178,7 +183,7 @@ func (t *Traits) UnmarshalJSON(data []byte) error { return nil } -func (i Identity) TableName(ctx context.Context) string { +func (i Identity) TableName(context.Context) string { return "identities" } @@ -237,20 +242,34 @@ func (i *Identity) DeleteCredentialsType(t CredentialsType) { delete(i.Credentials, t) } -func (i *Identity) GetCredentialsOr(t CredentialsType, or *Credentials) *Credentials { +// GetCredentialsOr returns the credentials for a given CredentialsType. If the +// credentials do not exist, the fallback is returned. +func (i *Identity) GetCredentialsOr(t CredentialsType, fallback *Credentials) *Credentials { c, ok := i.GetCredentials(t) if !ok { - return or + return fallback } return c } -func (i *Identity) UpsertCredentialsConfig(t CredentialsType, conf []byte, version int) { +type CredentialsOptions func(c *Credentials) + +func WithAdditionalIdentifier(identifier string) CredentialsOptions { + return func(c *Credentials) { + c.Identifiers = append(c.Identifiers, identifier) + } +} + +func (i *Identity) UpsertCredentialsConfig(t CredentialsType, conf []byte, version int, opt ...CredentialsOptions) { c, ok := i.GetCredentials(t) if !ok { c = &Credentials{} } + for _, optionFn := range opt { + optionFn(c) + } + c.Type = t c.IdentityID = i.ID c.Config = conf @@ -338,24 +357,27 @@ func (i *Identity) UnmarshalJSON(b []byte) error { return err } +// SetAvailableAAL sets the InternalAvailableAAL field based on the credentials stored in the identity. +// +// If a second factor is set up, the AAL will be set to 2. If only a first factor is set up, the AAL will be set to 1. +// +// A first factor is NOT required for the AAL to be set to 2 if a second factor is set up. func (i *Identity) SetAvailableAAL(ctx context.Context, m *Manager) (err error) { - i.AvailableAAL = NewNullableAuthenticatorAssuranceLevel(NoAuthenticatorAssuranceLevel) - if c, err := m.CountActiveFirstFactorCredentials(ctx, i); err != nil { + if c, err := m.CountActiveMultiFactorCredentials(ctx, i); err != nil { return err - } else if c == 0 { - // No first factor set up - AAL is 0 + } else if c > 0 { + i.InternalAvailableAAL = NewNullableAuthenticatorAssuranceLevel(AuthenticatorAssuranceLevel2) return nil } - i.AvailableAAL = NewNullableAuthenticatorAssuranceLevel(AuthenticatorAssuranceLevel1) - if c, err := m.CountActiveMultiFactorCredentials(ctx, i); err != nil { + if c, err := m.CountActiveFirstFactorCredentials(ctx, i); err != nil { return err - } else if c == 0 { - // No second factor set up - AAL is 1 + } else if c > 0 { + i.InternalAvailableAAL = NewNullableAuthenticatorAssuranceLevel(AuthenticatorAssuranceLevel1) return nil } - i.AvailableAAL = NewNullableAuthenticatorAssuranceLevel(AuthenticatorAssuranceLevel2) + i.InternalAvailableAAL = NewNullableAuthenticatorAssuranceLevel(NoAuthenticatorAssuranceLevel) return nil } @@ -448,49 +470,48 @@ func (i *Identity) WithDeclassifiedCredentials(ctx context.Context, c cipher.Pro toPublish := original toPublish.Config = []byte{} - for _, token := range []string{"initial_id_token", "initial_access_token", "initial_refresh_token"} { - var i int - var err error - gjson.GetBytes(original.Config, "providers").ForEach(func(_, v gjson.Result) bool { + var i int + var err error + gjson.GetBytes(original.Config, "providers").ForEach(func(_, v gjson.Result) bool { + for _, token := range []string{"initial_id_token", "initial_access_token", "initial_refresh_token"} { key := fmt.Sprintf("%d.%s", i, token) ciphertext := v.Get(token).String() var plaintext []byte - plaintext, err = c.Cipher(ctx).Decrypt(ctx, ciphertext) + plaintext, err := c.Cipher(ctx).Decrypt(ctx, ciphertext) if err != nil { - return false + plaintext = []byte("") } - toPublish.Config, err = sjson.SetBytes(toPublish.Config, "providers."+key, string(plaintext)) if err != nil { return false } + } - toPublish.Config, err = sjson.SetBytes(toPublish.Config, fmt.Sprintf("providers.%d.subject", i), v.Get("subject").String()) - if err != nil { - return false - } - - toPublish.Config, err = sjson.SetBytes(toPublish.Config, fmt.Sprintf("providers.%d.provider", i), v.Get("provider").String()) - if err != nil { - return false - } - - toPublish.Config, err = sjson.SetBytes(toPublish.Config, fmt.Sprintf("providers.%d.organization", i), v.Get("organization").String()) - if err != nil { - return false - } + toPublish.Config, err = sjson.SetBytes(toPublish.Config, fmt.Sprintf("providers.%d.subject", i), v.Get("subject").String()) + if err != nil { + return false + } - i++ - return true - }) + toPublish.Config, err = sjson.SetBytes(toPublish.Config, fmt.Sprintf("providers.%d.provider", i), v.Get("provider").String()) + if err != nil { + return false + } + toPublish.Config, err = sjson.SetBytes(toPublish.Config, fmt.Sprintf("providers.%d.organization", i), v.Get("organization").String()) if err != nil { - return nil, err + return false } - credsToPublish[ct] = toPublish + i++ + return true + }) + + if err != nil { + return nil, err } + + credsToPublish[ct] = toPublish default: credsToPublish[ct] = original } @@ -501,6 +522,80 @@ func (i *Identity) WithDeclassifiedCredentials(ctx context.Context, c cipher.Pro return &ii, nil } +func (i *Identity) deleteCredentialWebAuthFromIdentity() error { + cred, ok := i.GetCredentials(CredentialsTypeWebAuthn) + if !ok { + // This should never happend as it's checked earlier in the code; + // But we never know... + return errors.WithStack(herodot.ErrNotFound.WithReasonf("You tried to remove a WebAuthn credential but this user has no such credential set up.")) + } + + var cc CredentialsWebAuthnConfig + if err := json.Unmarshal(cred.Config, &cc); err != nil { + // Database has been tampered or the json schema are incompatible (migration issue); + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error())) + } + + updated := make([]CredentialWebAuthn, 0) + for k, cred := range cc.Credentials { + if cred.IsPasswordless { + updated = append(updated, cc.Credentials[k]) + } + } + + if len(updated) == 0 { + i.DeleteCredentialsType(CredentialsTypeWebAuthn) + return nil + } + + cc.Credentials = updated + message, err := json.Marshal(cc) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error())) + } + + cred.Config = message + i.SetCredentials(CredentialsTypeWebAuthn, *cred) + return nil +} + +func (i *Identity) deleteCredentialOIDCFromIdentity(identifierToDelete string) error { + if identifierToDelete == "" { + return errors.WithStack(herodot.ErrBadRequest.WithReasonf("You must provide an identifier to delete this credential.")) + } + _, hasOIDC := i.GetCredentials(CredentialsTypeOIDC) + if !hasOIDC { + return errors.WithStack(herodot.ErrNotFound.WithReasonf("You tried to remove an OIDC credential but this user has no such credential set up.")) + } + var oidcConfig CredentialsOIDC + creds, err := i.ParseCredentials(CredentialsTypeOIDC, &oidcConfig) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error())) + } + + var updatedIdentifiers []string + var updatedProviders []CredentialsOIDCProvider + var found bool + for _, cfg := range oidcConfig.Providers { + if identifierToDelete == OIDCUniqueID(cfg.Provider, cfg.Subject) { + found = true + continue + } + updatedIdentifiers = append(updatedIdentifiers, OIDCUniqueID(cfg.Provider, cfg.Subject)) + updatedProviders = append(updatedProviders, cfg) + } + if !found { + return errors.WithStack(herodot.ErrNotFound.WithReasonf("The identifier `%s` was not found among OIDC credentials.", identifierToDelete)) + } + creds.Identifiers = updatedIdentifiers + creds.Config, err = json.Marshal(&CredentialsOIDC{Providers: updatedProviders}) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error())) + } + i.Credentials[CredentialsTypeOIDC] = *creds + return nil +} + // Patch Identities Parameters // // swagger:parameters batchPatchIdentities @@ -554,6 +649,9 @@ const ( // Create this identity. ActionCreate BatchPatchAction = "create" + // Error indicates that the patch failed. + ActionError BatchPatchAction = "error" + // Future actions: // // Delete this identity. @@ -586,4 +684,7 @@ type BatchIdentityPatchResponse struct { // The ID of this patch response, if an ID was specified in the patch. PatchID *uuid.UUID `json:"patch_id,omitempty"` + + // The error message, if the action was "error". + Error *herodot.DefaultError `json:"error,omitempty"` } diff --git a/identity/identity_test.go b/identity/identity_test.go index 22b1cb43fe89..34a72fbb7868 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -5,24 +5,21 @@ package identity import ( "bytes" + "context" "encoding/json" "fmt" "testing" - "github.com/ory/x/snapshotx" - - "github.com/ory/kratos/x" - + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" - "github.com/gofrs/uuid" - - "github.com/ory/x/sqlxx" - + "github.com/ory/kratos/cipher" "github.com/ory/kratos/driver/config" - - "github.com/stretchr/testify/assert" + "github.com/ory/kratos/x" + "github.com/ory/x/snapshotx" + "github.com/ory/x/sqlxx" ) func TestNewIdentity(t *testing.T) { @@ -316,6 +313,12 @@ func TestVerifiableAddresses(t *testing.T) { assert.Equal(t, addresses, CollectVerifiableAddresses([]*Identity{id1, id2, id3})) } +type cipherProvider struct{} + +func (c *cipherProvider) Cipher(ctx context.Context) cipher.Cipher { + return cipher.NewNoop() +} + func TestWithDeclassifiedCredentials(t *testing.T) { i := NewIdentity(config.DefaultIdentityTraitsSchemaID) credentials := map[CredentialsType]Credentials{ @@ -327,7 +330,7 @@ func TestWithDeclassifiedCredentials(t *testing.T) { CredentialsTypeOIDC: { Type: CredentialsTypeOIDC, Identifiers: []string{"bar", "baz"}, - Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}"), + Config: sqlxx.JSONRawMessage(`{"providers": [{"initial_id_token": "666f6f"}]}`), }, CredentialsTypeWebAuthn: { Type: CredentialsTypeWebAuthn, @@ -338,7 +341,7 @@ func TestWithDeclassifiedCredentials(t *testing.T) { i.Credentials = credentials t.Run("case=no-include", func(t *testing.T) { - actualIdentity, err := i.WithDeclassifiedCredentials(ctx, nil, nil) + actualIdentity, err := i.WithDeclassifiedCredentials(ctx, &cipherProvider{}, nil) require.NoError(t, err) for ct, actual := range actualIdentity.Credentials { @@ -349,7 +352,7 @@ func TestWithDeclassifiedCredentials(t *testing.T) { }) t.Run("case=include-webauthn", func(t *testing.T) { - actualIdentity, err := i.WithDeclassifiedCredentials(ctx, nil, []CredentialsType{CredentialsTypeWebAuthn}) + actualIdentity, err := i.WithDeclassifiedCredentials(ctx, &cipherProvider{}, []CredentialsType{CredentialsTypeWebAuthn}) require.NoError(t, err) for ct, actual := range actualIdentity.Credentials { @@ -360,7 +363,18 @@ func TestWithDeclassifiedCredentials(t *testing.T) { }) t.Run("case=include-multi", func(t *testing.T) { - actualIdentity, err := i.WithDeclassifiedCredentials(ctx, nil, []CredentialsType{CredentialsTypeWebAuthn, CredentialsTypePassword}) + actualIdentity, err := i.WithDeclassifiedCredentials(ctx, &cipherProvider{}, []CredentialsType{CredentialsTypeWebAuthn, CredentialsTypePassword}) + require.NoError(t, err) + + for ct, actual := range actualIdentity.Credentials { + t.Run("credential="+string(ct), func(t *testing.T) { + snapshotx.SnapshotT(t, actual) + }) + } + }) + + t.Run("case=oidc", func(t *testing.T) { + actualIdentity, err := i.WithDeclassifiedCredentials(ctx, &cipherProvider{}, []CredentialsType{CredentialsTypeOIDC}) require.NoError(t, err) for ct, actual := range actualIdentity.Credentials { @@ -370,3 +384,58 @@ func TestWithDeclassifiedCredentials(t *testing.T) { } }) } + +func TestDeleteCredentialOIDCFromIdentity(t *testing.T) { + i := NewIdentity(config.DefaultIdentityTraitsSchemaID) + + err := i.deleteCredentialOIDCFromIdentity("") + assert.Error(t, err) + err = i.deleteCredentialOIDCFromIdentity("does-not-exist") + assert.Error(t, err) + + credentials := map[CredentialsType]Credentials{ + CredentialsTypePassword: { + Identifiers: []string{"zab", "bar"}, + Type: CredentialsTypePassword, + Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}"), + }, + CredentialsTypeOIDC: { + Type: CredentialsTypeOIDC, + Identifiers: []string{"bar:1234", "baz:5678"}, + Config: sqlxx.JSONRawMessage(`{"providers": [{"provider": "bar", "subject": "1234"}, {"provider": "baz", "subject": "5678"}]}`), + }, + CredentialsTypeWebAuthn: { + Type: CredentialsTypeWebAuthn, + Identifiers: []string{"foo", "bar"}, + Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}"), + }, + } + i.Credentials = credentials + + err = i.deleteCredentialOIDCFromIdentity("zab") + assert.Error(t, err) + err = i.deleteCredentialOIDCFromIdentity("foo") + assert.Error(t, err) + err = i.deleteCredentialOIDCFromIdentity("bar") + assert.Error(t, err, "matches multiple OIDC credentials") + + require.NoError(t, i.deleteCredentialOIDCFromIdentity("bar:1234")) + + assert.Len(t, i.Credentials, 3) + + assert.Contains(t, i.Credentials, CredentialsTypePassword) + assert.EqualValues(t, i.Credentials[CredentialsTypePassword].Identifiers, []string{"zab", "bar"}) + + assert.Contains(t, i.Credentials, CredentialsTypeWebAuthn) + assert.EqualValues(t, i.Credentials[CredentialsTypeWebAuthn].Identifiers, []string{"foo", "bar"}) + + assert.Contains(t, i.Credentials, CredentialsTypeOIDC) + + oidc, ok := i.GetCredentials(CredentialsTypeOIDC) + require.True(t, ok) + assert.EqualValues(t, oidc.Identifiers, []string{"baz:5678"}) + var cfg CredentialsOIDC + _, err = i.ParseCredentials(CredentialsTypeOIDC, &cfg) + require.NoError(t, err) + assert.EqualValues(t, CredentialsOIDC{Providers: []CredentialsOIDCProvider{{Provider: "baz", Subject: "5678"}}}, cfg) +} diff --git a/identity/identity_verification.go b/identity/identity_verification.go index 54ac435ec9fd..251fee019d3b 100644 --- a/identity/identity_verification.go +++ b/identity/identity_verification.go @@ -118,5 +118,5 @@ func (a VerifiableAddress) ValidateNID() error { // Hash returns a unique string representation for the recovery address. func (a VerifiableAddress) Hash() string { - return fmt.Sprintf("%v|%v|%v|%v|%v|%v", a.Value, a.Verified, a.Via, a.Status, a.IdentityID, a.NID) + return fmt.Sprintf("%v|%v|%v|%v|%v|%v|%v", a.Value, a.Verified, a.Via, a.Status, a.VerifiedAt, a.IdentityID, a.NID) } diff --git a/identity/identity_verification_test.go b/identity/identity_verification_test.go index 4559a5759dba..6f3ca2c69524 100644 --- a/identity/identity_verification_test.go +++ b/identity/identity_verification_test.go @@ -34,10 +34,10 @@ func TestNewVerifiableEmailAddress(t *testing.T) { } var tagsIgnoredForHashing = map[string]struct{}{ - "id": {}, - "created_at": {}, - "updated_at": {}, - "verified_at": {}, + "id": {}, + "created_at": {}, + "updated_at": {}, + // "verified_at": {}, // we explicitly want to be able to update just this field and nothing else } func reflectiveHash(record any) string { @@ -102,5 +102,4 @@ func TestVerifiableAddress_Hash(t *testing.T) { ) }) } - } diff --git a/identity/manager.go b/identity/manager.go index 9bd02ce7451c..89c0259e6658 100644 --- a/identity/manager.go +++ b/identity/manager.go @@ -6,13 +6,12 @@ package identity import ( "context" "encoding/json" + "fmt" "reflect" + "slices" "sort" - "go.opentelemetry.io/otel/trace" - "github.com/ory/kratos/schema" - "github.com/ory/kratos/x/events" "github.com/ory/x/sqlcon" "github.com/ory/x/otelx" @@ -28,8 +27,6 @@ import ( "github.com/ory/herodot" "github.com/ory/jsonschema/v3" - "github.com/ory/x/errorsx" - "github.com/ory/kratos/courier" ) @@ -95,10 +92,6 @@ func (m *Manager) Create(ctx context.Context, i *Identity, opts ...ManagerOption return err } - if err := i.SetAvailableAAL(ctx, m); err != nil { - return err - } - if err := m.r.PrivilegedIdentityPool().CreateIdentity(ctx, i); err != nil { if errors.Is(err, sqlcon.ErrUniqueViolation) { return m.findExistingAuthMethod(ctx, err, i) @@ -106,7 +99,6 @@ func (m *Manager) Create(ctx context.Context, i *Identity, opts ...ManagerOption return err } - trace.SpanFromContext(ctx).AddEvent(events.NewIdentityCreated(ctx, i.ID)) return nil } @@ -188,6 +180,8 @@ func (m *Manager) findExistingAuthMethod(ctx context.Context, e error, i *Identi return creds[i].Type < creds[j].Type }) + duplicateCredErr := &ErrDuplicateCredentials{error: e} + for _, cred := range creds { if cred.Config == nil { continue @@ -202,11 +196,28 @@ func (m *Manager) findExistingAuthMethod(ctx context.Context, e error, i *Identi if len(cred.Identifiers) > 0 { identifierHint = cred.Identifiers[0] } - return &ErrDuplicateCredentials{ - error: e, - availableCredentials: []CredentialsType{cred.Type}, - identifierHint: identifierHint, + duplicateCredErr.SetIdentifierHint(identifierHint) + + var cfg CredentialsPassword + if err := json.Unmarshal(cred.Config, &cfg); err != nil { + // just ignore this credential if the config is invalid + continue } + if cfg.HashedPassword == "" { + // just ignore this credential if the hashed password is empty + continue + } + + duplicateCredErr.AddCredentialsType(cred.Type) + + case CredentialsTypeCodeAuth: + identifierHint := foundConflictAddress + if len(cred.Identifiers) > 0 { + identifierHint = cred.Identifiers[0] + } + + duplicateCredErr.SetIdentifierHint(identifierHint) + duplicateCredErr.AddCredentialsType(cred.Type) case CredentialsTypeOIDC: var cfg CredentialsOIDC if err := json.Unmarshal(cred.Config, &cfg); err != nil { @@ -218,12 +229,9 @@ func (m *Manager) findExistingAuthMethod(ctx context.Context, e error, i *Identi available = append(available, provider.Provider) } - return &ErrDuplicateCredentials{ - error: e, - availableCredentials: []CredentialsType{cred.Type}, - availableOIDCProviders: available, - identifierHint: foundConflictAddress, - } + duplicateCredErr.AddCredentialsType(cred.Type) + duplicateCredErr.SetIdentifierHint(foundConflictAddress) + duplicateCredErr.availableOIDCProviders = available case CredentialsTypeWebAuthn: var cfg CredentialsWebAuthnConfig if err := json.Unmarshal(cred.Config, &cfg); err != nil { @@ -237,18 +245,33 @@ func (m *Manager) findExistingAuthMethod(ctx context.Context, e error, i *Identi for _, webauthn := range cfg.Credentials { if webauthn.IsPasswordless { - return &ErrDuplicateCredentials{ - error: e, - availableCredentials: []CredentialsType{cred.Type}, - identifierHint: identifierHint, - } + duplicateCredErr.AddCredentialsType(cred.Type) + duplicateCredErr.SetIdentifierHint(identifierHint) + break + } + } + case CredentialsTypePasskey: + var cfg CredentialsWebAuthnConfig + if err := json.Unmarshal(cred.Config, &cfg); err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to JSON decode identity credentials %s for identity %s.", cred.Type, found.ID)) + } + + identifierHint := foundConflictAddress + if len(cred.Identifiers) > 0 { + identifierHint = cred.Identifiers[0] + } + + for _, webauthn := range cfg.Credentials { + if webauthn.IsPasswordless { + duplicateCredErr.AddCredentialsType(cred.Type) + duplicateCredErr.SetIdentifierHint(identifierHint) + break } } } } - // Still not found? Return generic error. - return &ErrDuplicateCredentials{error: e} + return duplicateCredErr } type ErrDuplicateCredentials struct { @@ -265,53 +288,127 @@ func (e *ErrDuplicateCredentials) Unwrap() error { return e.error } +func (e *ErrDuplicateCredentials) AddCredentialsType(ct CredentialsType) { + e.availableCredentials = append(e.availableCredentials, ct) +} + +func (e *ErrDuplicateCredentials) SetIdentifierHint(hint string) { + if hint != "" { + e.identifierHint = hint + } +} + func (e *ErrDuplicateCredentials) AvailableCredentials() []string { res := make([]string, len(e.availableCredentials)) for k, v := range e.availableCredentials { res[k] = string(v) } + slices.Sort(res) + return res } func (e *ErrDuplicateCredentials) AvailableOIDCProviders() []string { + if e.availableOIDCProviders == nil { + return []string{} + } + slices.Sort(e.availableOIDCProviders) return e.availableOIDCProviders } func (e *ErrDuplicateCredentials) IdentifierHint() string { return e.identifierHint } + func (e *ErrDuplicateCredentials) HasHints() bool { return len(e.availableCredentials) > 0 || len(e.availableOIDCProviders) > 0 || len(e.identifierHint) > 0 } +type FailedIdentity struct { + Identity *Identity + Error *herodot.DefaultError +} + +type CreateIdentitiesError struct { + failedIdentities map[*Identity]*herodot.DefaultError +} + +func (e *CreateIdentitiesError) Error() string { + e.init() + return fmt.Sprintf("create identities error: %d identities failed", len(e.failedIdentities)) +} +func (e *CreateIdentitiesError) Unwrap() []error { + e.init() + var errs []error + for _, err := range e.failedIdentities { + errs = append(errs, err) + } + return errs +} + +func (e *CreateIdentitiesError) AddFailedIdentity(ident *Identity, err *herodot.DefaultError) { + e.init() + e.failedIdentities[ident] = err +} +func (e *CreateIdentitiesError) Merge(other *CreateIdentitiesError) { + e.init() + for k, v := range other.failedIdentities { + e.failedIdentities[k] = v + } +} +func (e *CreateIdentitiesError) Contains(ident *Identity) bool { + e.init() + _, found := e.failedIdentities[ident] + return found +} +func (e *CreateIdentitiesError) Find(ident *Identity) *FailedIdentity { + e.init() + if err, found := e.failedIdentities[ident]; found { + return &FailedIdentity{Identity: ident, Error: err} + } + + return nil +} +func (e *CreateIdentitiesError) ErrOrNil() error { + if len(e.failedIdentities) == 0 { + return nil + } + return e +} +func (e *CreateIdentitiesError) init() { + if e.failedIdentities == nil { + e.failedIdentities = map[*Identity]*herodot.DefaultError{} + } +} + func (m *Manager) CreateIdentities(ctx context.Context, identities []*Identity, opts ...ManagerOption) (err error) { ctx, span := m.r.Tracer(ctx).Tracer().Start(ctx, "identity.Manager.CreateIdentities") defer otelx.End(span, &err) - for _, i := range identities { - if i.SchemaID == "" { - i.SchemaID = m.r.Config().DefaultIdentityTraitsSchemaID(ctx) - } - - if err := i.SetAvailableAAL(ctx, m); err != nil { - return err + createIdentitiesError := &CreateIdentitiesError{} + validIdentities := make([]*Identity, 0, len(identities)) + for _, ident := range identities { + if ident.SchemaID == "" { + ident.SchemaID = m.r.Config().DefaultIdentityTraitsSchemaID(ctx) } o := newManagerOptions(opts) - if err := m.ValidateIdentity(ctx, i, o); err != nil { - return err + if err := m.ValidateIdentity(ctx, ident, o); err != nil { + createIdentitiesError.AddFailedIdentity(ident, herodot.ErrBadRequest.WithReasonf("%s", err).WithWrap(err)) + continue } + validIdentities = append(validIdentities, ident) } - if err := m.r.PrivilegedIdentityPool().CreateIdentities(ctx, identities...); err != nil { - return err - } - - for _, i := range identities { - trace.SpanFromContext(ctx).AddEvent(events.NewIdentityCreated(ctx, i.ID)) + if err := m.r.PrivilegedIdentityPool().CreateIdentities(ctx, validIdentities...); err != nil { + if partialErr := new(CreateIdentitiesError); errors.As(err, &partialErr) { + createIdentitiesError.Merge(partialErr) + } else { + return err + } } - return nil + return createIdentitiesError.ErrOrNil() } func (m *Manager) requiresPrivilegedAccess(ctx context.Context, original, updated *Identity, o *ManagerOptions) (err error) { @@ -322,6 +419,7 @@ func (m *Manager) requiresPrivilegedAccess(ctx context.Context, original, update if !CredentialsEqual(updated.Credentials, original.Credentials) { // reset the identity *updated = *original + return errors.WithStack(ErrProtectedFieldModified) } @@ -354,10 +452,6 @@ func (m *Manager) Update(ctx context.Context, updated *Identity, opts ...Manager return err } - if err := updated.SetAvailableAAL(ctx, m); err != nil { - return err - } - return m.r.PrivilegedIdentityPool().UpdateIdentity(ctx, updated) } @@ -380,7 +474,6 @@ func (m *Manager) UpdateSchemaID(ctx context.Context, id uuid.UUID, schemaID str return err } - trace.SpanFromContext(ctx).AddEvent(events.NewIdentityUpdated(ctx, id)) return m.r.PrivilegedIdentityPool().UpdateIdentity(ctx, original) } @@ -408,6 +501,30 @@ func (m *Manager) SetTraits(ctx context.Context, id uuid.UUID, traits Traits, op return updated, nil } +// RefreshAvailableAAL refreshes the available AAL for the identity. +// +// This method is a no-op if everything is up-to date. +// +// Please make sure to load all credentials before using this method. +func (m *Manager) RefreshAvailableAAL(ctx context.Context, i *Identity) (err error) { + if len(i.Credentials) == 0 { + if err := m.r.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, i, ExpandCredentials); err != nil { + return err + } + } + + aalBefore := i.InternalAvailableAAL + if err := i.SetAvailableAAL(ctx, m); err != nil { + return err + } + + if aalBefore.String != i.InternalAvailableAAL.String || aalBefore.Valid != i.InternalAvailableAAL.Valid { + return m.r.PrivilegedIdentityPool().UpdateIdentityColumns(ctx, i, "available_aal") + } + + return nil +} + func (m *Manager) UpdateTraits(ctx context.Context, id uuid.UUID, traits Traits, opts ...ManagerOption) (err error) { ctx, span := m.r.Tracer(ctx).Tracer().Start(ctx, "identity.Manager.UpdateTraits") defer otelx.End(span, &err) @@ -417,22 +534,22 @@ func (m *Manager) UpdateTraits(ctx context.Context, id uuid.UUID, traits Traits, return err } - trace.SpanFromContext(ctx).AddEvent(events.NewIdentityUpdated(ctx, id)) return m.r.PrivilegedIdentityPool().UpdateIdentity(ctx, updated) } func (m *Manager) ValidateIdentity(ctx context.Context, i *Identity, o *ManagerOptions) (err error) { - // This trace is more noisy than it's worth in diagnostic power. - // ctx, span := m.r.Tracer(ctx).Tracer().Start(ctx, "identity.Manager.validate") - // defer otelx.End(span, &err) - if err := m.r.IdentityValidator().Validate(ctx, i); err != nil { - if _, ok := errorsx.Cause(err).(*jsonschema.ValidationError); ok && !o.ExposeValidationErrors { + var validationErr *jsonschema.ValidationError + if errors.As(err, &validationErr) && !o.ExposeValidationErrors { return herodot.ErrBadRequest.WithReasonf("%s", err).WithWrap(err) } return err } + if err := i.SetAvailableAAL(ctx, m); err != nil { + return err + } + return nil } @@ -442,7 +559,7 @@ func (m *Manager) CountActiveFirstFactorCredentials(ctx context.Context, i *Iden // defer otelx.End(span, &err) for _, strategy := range m.r.ActiveCredentialsCounterStrategies(ctx) { - current, err := strategy.CountActiveFirstFactorCredentials(i.Credentials) + current, err := strategy.CountActiveFirstFactorCredentials(ctx, i.Credentials) if err != nil { return 0, err } @@ -458,7 +575,7 @@ func (m *Manager) CountActiveMultiFactorCredentials(ctx context.Context, i *Iden // defer otelx.End(span, &err) for _, strategy := range m.r.ActiveCredentialsCounterStrategies(ctx) { - current, err := strategy.CountActiveMultiFactorCredentials(i.Credentials) + current, err := strategy.CountActiveMultiFactorCredentials(ctx, i.Credentials) if err != nil { return 0, err } diff --git a/identity/manager_test.go b/identity/manager_test.go index 81001c9ba9c6..bd9e67782ea9 100644 --- a/identity/manager_test.go +++ b/identity/manager_test.go @@ -4,14 +4,17 @@ package identity_test import ( - "context" + "encoding/json" "fmt" "testing" "time" + "github.com/ory/x/configx" "github.com/ory/x/pointerx" "github.com/ory/x/sqlcon" + _ "embed" + "github.com/gofrs/uuid" "github.com/ory/x/sqlxx" @@ -28,18 +31,21 @@ import ( "github.com/ory/kratos/x" ) +//go:embed stub/aal.json +var refreshAALStubs []byte + func TestManager(t *testing.T) { - conf, reg := internal.NewFastRegistryWithMocks(t) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/manager.schema.json") - extensionSchemaID := testhelpers.UseIdentitySchema(t, conf, "file://./stub/extension.schema.json") - conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") - conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") - conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationLoginHints, true) + conf, reg := internal.NewFastRegistryWithMocks(t, configx.WithValues(map[string]interface{}{ + config.ViperKeyPublicBaseURL: "https://www.ory.sh/", + config.ViperKeyCourierSMTPURL: "smtp://foo@bar@dev.null/", + config.ViperKeySelfServiceRegistrationLoginHints: true, + }), configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/manager.schema.json"))) + ctx, extensionSchemaID := testhelpers.WithAddIdentitySchema(ctx, t, conf, "file://./stub/extension.schema.json") t.Run("case=should fail to create because validation fails", func(t *testing.T) { i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) i.Traits = identity.Traits(`{"email":"not an email"}`) - require.Error(t, reg.IdentityManager().Create(context.Background(), i)) + require.Error(t, reg.IdentityManager().Create(ctx, i)) }) newTraits := func(email string, unprotected string) identity.Traits { @@ -62,7 +68,7 @@ func TestManager(t *testing.T) { } checkExtensionFieldsForIdentities := func(t *testing.T, expected string, original *identity.Identity) { - fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID) + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID) require.NoError(t, err) identities := []identity.Identity{*original, *fromStore} for k := range identities { @@ -70,14 +76,45 @@ func TestManager(t *testing.T) { } } + t.Run("method=CreateIdentities", func(t *testing.T) { + t.Run("case=should set AAL to 2 if password and TOTP is set", func(t *testing.T) { + email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" + original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + original.Traits = newTraits(email, "") + original.Credentials = map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + // By explicitly not setting the identifier, we mimic the behavior of the PATCH endpoint. + // This tests a bug we introduced on the PATCH endpoint where the AAL value would not be correct. + Identifiers: []string{}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), + }, + identity.CredentialsTypeTOTP: { + Type: identity.CredentialsTypeTOTP, + // By explicitly not setting the identifier, we mimic the behavior of the PATCH endpoint. + // This tests a bug we introduced on the PATCH endpoint where the AAL value would not be correct. + Identifiers: []string{}, + Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`), + }, + } + require.NoError(t, reg.IdentityManager().CreateIdentities(ctx, []*identity.Identity{original})) + fromStore, err := reg.PrivilegedIdentityPool().GetIdentity(ctx, original.ID, identity.ExpandNothing) + require.NoError(t, err) + + got, ok := fromStore.InternalAvailableAAL.ToAAL() + require.True(t, ok) + assert.Equal(t, identity.AuthenticatorAssuranceLevel2, got) + }) + }) + t.Run("method=Create", func(t *testing.T) { t.Run("case=should create identity and track extension fields", func(t *testing.T) { email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits(email, "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) checkExtensionFieldsForIdentities(t, email, original) - got, ok := original.AvailableAAL.ToAAL() + got, ok := original.InternalAvailableAAL.ToAAL() require.True(t, ok) assert.Equal(t, identity.NoAuthenticatorAssuranceLevel, got) }) @@ -87,8 +124,8 @@ func TestManager(t *testing.T) { email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits(email, "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) - got, ok := original.AvailableAAL.ToAAL() + require.NoError(t, reg.IdentityManager().Create(ctx, original)) + got, ok := original.InternalAvailableAAL.ToAAL() require.True(t, ok) assert.Equal(t, identity.NoAuthenticatorAssuranceLevel, got) }) @@ -104,8 +141,8 @@ func TestManager(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), }, } - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) - got, ok := original.AvailableAAL.ToAAL() + require.NoError(t, reg.IdentityManager().Create(ctx, original)) + got, ok := original.InternalAvailableAAL.ToAAL() require.True(t, ok) assert.Equal(t, identity.AuthenticatorAssuranceLevel1, got) }) @@ -126,13 +163,13 @@ func TestManager(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`), }, } - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) - got, ok := original.AvailableAAL.ToAAL() + require.NoError(t, reg.IdentityManager().Create(ctx, original)) + got, ok := original.InternalAvailableAAL.ToAAL() require.True(t, ok) assert.Equal(t, identity.AuthenticatorAssuranceLevel2, got) }) - t.Run("case=should set AAL to 0 if only TOTP is set", func(t *testing.T) { + t.Run("case=should set AAL to 2 if only TOTP is set", func(t *testing.T) { email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits(email, "") @@ -143,17 +180,17 @@ func TestManager(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`), }, } - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) - got, ok := original.AvailableAAL.ToAAL() + require.NoError(t, reg.IdentityManager().Create(ctx, original)) + got, ok := original.InternalAvailableAAL.ToAAL() require.True(t, ok) - assert.Equal(t, identity.NoAuthenticatorAssuranceLevel, got) + assert.Equal(t, identity.AuthenticatorAssuranceLevel2, got) }) }) t.Run("case=should expose validation errors with option", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = identity.Traits(`{"email":"not an email"}`) - err := reg.IdentityManager().Create(context.Background(), original, identity.ManagerExposeValidationErrorsForInternalTypeAssertion) + err := reg.IdentityManager().Create(ctx, original, identity.ManagerExposeValidationErrorsForInternalTypeAssertion) require.Error(t, err) assert.Contains(t, err.Error(), "\"not an email\" is not valid \"email\"") }) @@ -161,7 +198,7 @@ func TestManager(t *testing.T) { t.Run("case=should not expose validation errors without option", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = identity.Traits(`{"email":"not an email"}`) - err := reg.IdentityManager().Create(context.Background(), original) + err := reg.IdentityManager().Create(ctx, original) require.Error(t, err) assert.NotContains(t, err.Error(), "\"not an email\" is not valid \"email\"") }) @@ -186,13 +223,13 @@ func TestManager(t *testing.T) { } first := createIdentity(email, "email_creds", creds) - require.NoError(t, reg.IdentityManager().Create(context.Background(), first)) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) second := createIdentity(email, "email_creds", creds) - err := reg.IdentityManager().Create(context.Background(), second) + err := reg.IdentityManager().Create(ctx, second) require.Error(t, err) - var verr = new(identity.ErrDuplicateCredentials) + verr := new(identity.ErrDuplicateCredentials) assert.ErrorAs(t, err, &verr) assert.EqualValues(t, []string{identity.CredentialsTypePassword.String()}, verr.AvailableCredentials()) assert.Len(t, verr.AvailableOIDCProviders(), 0) @@ -210,18 +247,102 @@ func TestManager(t *testing.T) { } first := createIdentity(email, "email_webauthn", creds) - require.NoError(t, reg.IdentityManager().Create(context.Background(), first)) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) second := createIdentity(email, "email_webauthn", nil) - err := reg.IdentityManager().Create(context.Background(), second) + err := reg.IdentityManager().Create(ctx, second) require.Error(t, err) - var verr = new(identity.ErrDuplicateCredentials) + verr := new(identity.ErrDuplicateCredentials) assert.ErrorAs(t, err, &verr) assert.EqualValues(t, []string{identity.CredentialsTypeWebAuthn.String()}, verr.AvailableCredentials()) assert.Len(t, verr.AvailableOIDCProviders(), 0) assert.Equal(t, verr.IdentifierHint(), email) }) + + t.Run("type=oidc", func(t *testing.T) { + email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" + creds := map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, + // Identifiers in OIDC are not email addresses, but a unique user ID. + Identifiers: []string{"google:" + uuid.Must(uuid.NewV4()).String()}, + Config: sqlxx.JSONRawMessage(`{"providers":[{"provider": "google"},{"provider": "github"}]}`), + }, + } + + first := createIdentity(email, "email_creds", creds) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) + + second := createIdentity(email, "email_creds", creds) + err := reg.IdentityManager().Create(ctx, second) + require.Error(t, err) + + verr := new(identity.ErrDuplicateCredentials) + assert.ErrorAs(t, err, &verr) + assert.ElementsMatch(t, []string{"oidc"}, verr.AvailableCredentials()) + assert.ElementsMatch(t, []string{"google", "github"}, verr.AvailableOIDCProviders()) + assert.Equal(t, email, verr.IdentifierHint()) + }) + + t.Run("type=password+oidc+webauthn", func(t *testing.T) { + email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" + creds := map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), + }, + identity.CredentialsTypeOIDC: { + Type: identity.CredentialsTypeOIDC, + // Identifiers in OIDC are not email addresses, but a unique user ID. + Identifiers: []string{"google:" + uuid.Must(uuid.NewV4()).String()}, + Config: sqlxx.JSONRawMessage(`{"providers":[{"provider": "google"},{"provider": "github"}]}`), + }, + identity.CredentialsTypeWebAuthn: { + Type: identity.CredentialsTypeWebAuthn, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{"credentials": [{"is_passwordless":true}]}`), + }, + } + + first := createIdentity(email, "email_creds", creds) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) + + second := createIdentity(email, "email_creds", creds) + err := reg.IdentityManager().Create(ctx, second) + require.Error(t, err) + + verr := new(identity.ErrDuplicateCredentials) + assert.ErrorAs(t, err, &verr) + assert.ElementsMatch(t, []string{"password", "oidc", "webauthn"}, verr.AvailableCredentials()) + assert.ElementsMatch(t, []string{"google", "github"}, verr.AvailableOIDCProviders()) + assert.Equal(t, email, verr.IdentifierHint()) + }) + + t.Run("type=code", func(t *testing.T) { + email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" + creds := map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeCodeAuth: { + Type: identity.CredentialsTypeCodeAuth, + Identifiers: []string{email}, + Config: sqlxx.JSONRawMessage(`{}`), + }, + } + + first := createIdentity(email, "email_creds", creds) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) + + second := createIdentity(email, "email_creds", creds) + err := reg.IdentityManager().Create(ctx, second) + require.Error(t, err) + + verr := new(identity.ErrDuplicateCredentials) + assert.ErrorAs(t, err, &verr) + assert.EqualValues(t, []string{identity.CredentialsTypeCodeAuth.String()}, verr.AvailableCredentials()) + assert.Len(t, verr.AvailableOIDCProviders(), 0) + assert.Equal(t, verr.IdentifierHint(), email) + }) }) runAddress := func(t *testing.T, field string) { @@ -240,13 +361,13 @@ func TestManager(t *testing.T) { } first := createIdentity(email, field, creds) - require.NoError(t, reg.IdentityManager().Create(context.Background(), first)) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) second := createIdentity(email, field, nil) - err := reg.IdentityManager().Create(context.Background(), second) + err := reg.IdentityManager().Create(ctx, second) require.Error(t, err) - var verr = new(identity.ErrDuplicateCredentials) + verr := new(identity.ErrDuplicateCredentials) assert.ErrorAs(t, err, &verr) assert.EqualValues(t, []string{identity.CredentialsTypePassword.String()}, verr.AvailableCredentials()) assert.Len(t, verr.AvailableOIDCProviders(), 0) @@ -269,16 +390,16 @@ func TestManager(t *testing.T) { } first := createIdentity(email, field, creds) - require.NoError(t, reg.IdentityManager().Create(context.Background(), first)) + require.NoError(t, reg.IdentityManager().Create(ctx, first)) second := createIdentity(email, field, nil) - err := reg.IdentityManager().Create(context.Background(), second) + err := reg.IdentityManager().Create(ctx, second) require.Error(t, err) - var verr = new(identity.ErrDuplicateCredentials) + verr := new(identity.ErrDuplicateCredentials) assert.ErrorAs(t, err, &verr) assert.EqualValues(t, []string{identity.CredentialsTypeOIDC.String()}, verr.AvailableCredentials()) - assert.EqualValues(t, verr.AvailableOIDCProviders(), []string{"google", "github"}) + assert.EqualValues(t, verr.AvailableOIDCProviders(), []string{"github", "google"}) assert.Equal(t, verr.IdentifierHint(), email) }) } @@ -297,10 +418,10 @@ func TestManager(t *testing.T) { t.Run("case=should update identity and update extension fields", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits("baz@ory.sh", "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) original.Traits = newTraits("bar@ory.sh", "") - require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits)) + require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits)) checkExtensionFieldsForIdentities(t, "bar@ory.sh", original) }) @@ -309,7 +430,7 @@ func TestManager(t *testing.T) { email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits(email, "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) original.Credentials = map[identity.CredentialsType]identity.Credentials{ identity.CredentialsTypePassword: { Type: identity.CredentialsTypePassword, @@ -317,8 +438,8 @@ func TestManager(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), }, } - require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits)) - assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, original.AvailableAAL.String) + require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits)) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, original.InternalAvailableAAL.String) }) t.Run("case=should set AAL to 2 if password and TOTP is set", func(t *testing.T) { @@ -332,24 +453,24 @@ func TestManager(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), }, } - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) - assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, original.AvailableAAL.String) - require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits)) - assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, original.AvailableAAL.String, "Updating without changes should not change AAL") + require.NoError(t, reg.IdentityManager().Create(ctx, original)) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, original.InternalAvailableAAL.String) + require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits)) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, original.InternalAvailableAAL.String, "Updating without changes should not change AAL") original.Credentials[identity.CredentialsTypeTOTP] = identity.Credentials{ Type: identity.CredentialsTypeTOTP, Identifiers: []string{email}, Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`), } - require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits)) - assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, original.AvailableAAL.String) + require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits)) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, original.InternalAvailableAAL.String) }) - t.Run("case=should set AAL to 0 if only TOTP is set", func(t *testing.T) { + t.Run("case=should set AAL to 2 if only TOTP is set", func(t *testing.T) { email := uuid.Must(uuid.NewV4()).String() + "@ory.sh" original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits(email, "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) original.Credentials = map[identity.CredentialsType]identity.Credentials{ identity.CredentialsTypeTOTP: { Type: identity.CredentialsTypeTOTP, @@ -357,22 +478,22 @@ func TestManager(t *testing.T) { Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`), }, } - require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits)) - assert.True(t, original.AvailableAAL.Valid) - assert.EqualValues(t, identity.NoAuthenticatorAssuranceLevel, original.AvailableAAL.String) + require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits)) + assert.True(t, original.InternalAvailableAAL.Valid) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, original.InternalAvailableAAL.String) }) t.Run("case=should not update protected traits without option", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits("email-update-1@ory.sh", "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) original.Traits = newTraits("email-update-2@ory.sh", "") - err := reg.IdentityManager().Update(context.Background(), original) + err := reg.IdentityManager().Update(ctx, original) require.Error(t, err) assert.Equal(t, identity.ErrProtectedFieldModified, errors.Cause(err)) - fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID) + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID) require.NoError(t, err) // As UpdateTraits takes only the ID as a parameter it cannot update the identity in place. // That is why we only check the identity in the store. @@ -422,21 +543,21 @@ func TestManager(t *testing.T) { originalEmail := x.NewUUID().String() + "@ory.sh" original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits(originalEmail, "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) - fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID) + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID) require.NoError(t, err) checkExtensionFields(fromStore, originalEmail)(t) newEmail := x.NewUUID().String() + "@ory.sh" original.Traits = newTraits(newEmail, "") - require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits)) + require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits)) - fromStore, err = reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID) + fromStore, err = reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID) require.NoError(t, err) checkExtensionFields(fromStore, newEmail)(t) - recoveryAddresses, err := reg.PrivilegedIdentityPool().ListRecoveryAddresses(context.Background(), 0, 500) + recoveryAddresses, err := reg.PrivilegedIdentityPool().ListRecoveryAddresses(ctx, 0, 500) require.NoError(t, err) var foundRecoveryAddress bool @@ -448,7 +569,7 @@ func TestManager(t *testing.T) { } require.True(t, foundRecoveryAddress) - verifiableAddresses, err := reg.PrivilegedIdentityPool().ListVerifiableAddresses(context.Background(), 0, 500) + verifiableAddresses, err := reg.PrivilegedIdentityPool().ListVerifiableAddresses(ctx, 0, 500) require.NoError(t, err) var foundVerifiableAddress bool for _, a := range verifiableAddresses { @@ -509,13 +630,13 @@ func TestManager(t *testing.T) { t.Run("case=should update protected traits with option", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits("email-updatetraits-1@ory.sh", "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) require.NoError(t, reg.IdentityManager().UpdateTraits( - context.Background(), original.ID, newTraits("email-updatetraits-2@ory.sh", ""), + ctx, original.ID, newTraits("email-updatetraits-2@ory.sh", ""), identity.ManagerAllowWriteProtectedTraits)) - fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID) + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID) require.NoError(t, err) // As UpdateTraits takes only the ID as a parameter it cannot update the identity in place. // That is why we only check the identity in the store. @@ -525,17 +646,17 @@ func TestManager(t *testing.T) { t.Run("case=should update identity and update extension fields", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`) - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) // These should all fail because they modify existing keys - require.Error(t, reg.IdentityManager().UpdateTraits(context.Background(), original.ID, identity.Traits(`{"email":"not-baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`))) - require.Error(t, reg.IdentityManager().UpdateTraits(context.Background(), original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"not-baz@ory.sh","email_recovery":"not-baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`))) - require.Error(t, reg.IdentityManager().UpdateTraits(context.Background(), original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"not-baz@ory.sh","unprotected": "foo"}`))) + require.Error(t, reg.IdentityManager().UpdateTraits(ctx, original.ID, identity.Traits(`{"email":"not-baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`))) + require.Error(t, reg.IdentityManager().UpdateTraits(ctx, original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"not-baz@ory.sh","email_recovery":"not-baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`))) + require.Error(t, reg.IdentityManager().UpdateTraits(ctx, original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"not-baz@ory.sh","unprotected": "foo"}`))) - require.NoError(t, reg.IdentityManager().UpdateTraits(context.Background(), original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "bar"}`))) + require.NoError(t, reg.IdentityManager().UpdateTraits(ctx, original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "bar"}`))) checkExtensionFieldsForIdentities(t, "baz@ory.sh", original) - actual, err := reg.IdentityPool().GetIdentity(context.Background(), original.ID, identity.ExpandNothing) + actual, err := reg.IdentityPool().GetIdentity(ctx, original.ID, identity.ExpandNothing) require.NoError(t, err) assert.JSONEq(t, `{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "bar"}`, string(actual.Traits)) }) @@ -543,14 +664,14 @@ func TestManager(t *testing.T) { t.Run("case=should not update protected traits without option", func(t *testing.T) { original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) original.Traits = newTraits("email-updatetraits-1@ory.sh", "") - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) err := reg.IdentityManager().UpdateTraits( - context.Background(), original.ID, newTraits("email-updatetraits-2@ory.sh", "")) + ctx, original.ID, newTraits("email-updatetraits-2@ory.sh", "")) require.Error(t, err) assert.Equal(t, identity.ErrProtectedFieldModified, errors.Cause(err)) - fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID) + fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID) require.NoError(t, err) // As UpdateTraits takes only the ID as a parameter it cannot update the identity in place. // That is why we only check the identity in the store. @@ -558,8 +679,53 @@ func TestManager(t *testing.T) { }) }) + t.Run("method=RefreshAvailableAAL", func(t *testing.T) { + var cases []struct { + Credentials []identity.Credentials `json:"credentials"` + Description string `json:"description"` + Expected string `json:"expected"` + } + require.NoError(t, json.Unmarshal(refreshAALStubs, &cases)) + + for k, tc := range cases { + t.Run("case="+tc.Description, func(t *testing.T) { + email := x.NewUUID().String() + "@ory.sh" + id := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + id.Traits = identity.Traits(`{"email":"` + email + `"}`) + require.NoError(t, reg.IdentityManager().Create(ctx, id)) + assert.EqualValues(t, identity.NoAuthenticatorAssuranceLevel, id.InternalAvailableAAL.String) + + for _, c := range tc.Credentials { + for k := range c.Identifiers { + switch c.Identifiers[k] { + case "{email}": + c.Identifiers[k] = email + case "{id}": + c.Identifiers[k] = id.ID.String() + } + } + id.SetCredentials(c.Type, c) + } + + // We use the privileged pool here because we don't want to refresh AAL here but in the code below. + require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(ctx, id)) + + expand := identity.ExpandNothing + if k%2 == 1 { // expand every other test case to test if RefreshAvailableAAL behaves correctly + expand = identity.ExpandCredentials + } + + actual, err := reg.IdentityPool().GetIdentity(ctx, id.ID, expand) + require.NoError(t, err) + require.NoError(t, reg.IdentityManager().RefreshAvailableAAL(ctx, actual)) + assert.NotEmpty(t, actual.Credentials) + assert.EqualValues(t, tc.Expected, actual.InternalAvailableAAL.String) + }) + } + }) + t.Run("method=ConflictingIdentity", func(t *testing.T) { - ctx := context.Background() + ctx := ctx conflicOnIdentifier := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) conflicOnIdentifier.Traits = identity.Traits(`{"email":"conflict-on-identifier@example.com"}`) @@ -622,12 +788,13 @@ func TestManager(t *testing.T) { } func TestManagerNoDefaultNamedSchema(t *testing.T) { - conf, reg := internal.NewFastRegistryWithMocks(t) - conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "user_v0") - conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{ - {ID: "user_v0", URL: "file://./stub/manager.schema.json"}, - }) - conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") + _, reg := internal.NewFastRegistryWithMocks(t, configx.WithValues(map[string]interface{}{ + config.ViperKeyDefaultIdentitySchemaID: "user_v0", + config.ViperKeyIdentitySchemas: config.Schemas{ + {ID: "user_v0", URL: "file://./stub/manager.schema.json"}, + }, + config.ViperKeyPublicBaseURL: "https://www.ory.sh/", + })) t.Run("case=should create identity with default schema", func(t *testing.T) { stateChangedAt := sqlxx.NullTime(time.Now().UTC()) @@ -637,6 +804,6 @@ func TestManagerNoDefaultNamedSchema(t *testing.T) { State: identity.StateActive, StateChangedAt: &stateChangedAt, } - require.NoError(t, reg.IdentityManager().Create(context.Background(), original)) + require.NoError(t, reg.IdentityManager().Create(ctx, original)) }) } diff --git a/identity/pool.go b/identity/pool.go index 5316f8a53ff9..30a7308245b4 100644 --- a/identity/pool.go +++ b/identity/pool.go @@ -18,9 +18,10 @@ import ( type ( ListIdentityParameters struct { Expand Expandables - IdsFilter []string + IdsFilter []uuid.UUID CredentialsIdentifier string CredentialsIdentifierSimilar string + DeclassifyCredentials []CredentialsType KeySetPagination []keysetpagination.Option // DEPRECATED PagePagination *x.Page @@ -60,9 +61,13 @@ type ( FindByCredentialsIdentifier(ctx context.Context, ct CredentialsType, match string) (*Identity, *Credentials, error) // DeleteIdentity removes an identity by its id. Will return an error - // if identity exists, backend connectivity is broken, or trait validation fails. + // if identity does not exists, or backend connectivity is broken. DeleteIdentity(context.Context, uuid.UUID) error + // DeleteIdentities removes identities by its id. Will return an error + // if any identity does not exists, or backend connectivity is broken. + DeleteIdentities(context.Context, []uuid.UUID) error + // UpdateVerifiableAddress updates an identity's verifiable address. UpdateVerifiableAddress(ctx context.Context, address *VerifiableAddress) error @@ -77,7 +82,13 @@ type ( // UpdateIdentity updates an identity including its confidential / privileged / protected data. UpdateIdentity(context.Context, *Identity) error - // GetIdentityConfidential returns the identity including it's raw credentials. This should only be used internally. + // UpdateIdentityColumns updates targeted columns of an identity. + UpdateIdentityColumns(ctx context.Context, i *Identity, columns ...string) error + + // GetIdentityConfidential returns the identity including it's raw credentials. + // + // This should only be used internally. Please be aware that this method uses HydrateIdentityAssociations + // internally, which must not be executed as part of a transaction. GetIdentityConfidential(context.Context, uuid.UUID) (*Identity, error) // ListVerifiableAddresses lists all tracked verifiable addresses, regardless of whether they are already verified @@ -88,12 +99,18 @@ type ( ListRecoveryAddresses(ctx context.Context, page, itemsPerPage int) ([]RecoveryAddress, error) // HydrateIdentityAssociations hydrates the associations of an identity. + // + // Please be aware that this method must not be called within a transaction if more than one element is expanded. + // It may error with "conn busy" otherwise. HydrateIdentityAssociations(ctx context.Context, i *Identity, expandables Expandables) error // InjectTraitsSchemaURL sets the identity's traits JSON schema URL from the schema's ID. InjectTraitsSchemaURL(ctx context.Context, i *Identity) error - // FindIdentityByAnyCaseSensitiveCredentialIdentifier returns an identity by matching the identifier to any of the identity's credentials. + // FindIdentityByCredentialIdentifier returns an identity by matching the identifier to any of the identity's credentials. FindIdentityByCredentialIdentifier(ctx context.Context, identifier string, caseSensitive bool) (*Identity, error) + + // FindIdentityByWebauthnUserHandle returns an identity matching a webauthn user handle. + FindIdentityByWebauthnUserHandle(ctx context.Context, userHandle []byte) (*Identity, error) } ) diff --git a/identity/stub/aal.json b/identity/stub/aal.json new file mode 100644 index 000000000000..bb206c54b395 --- /dev/null +++ b/identity/stub/aal.json @@ -0,0 +1,76 @@ +[ + { + "description": "password is available aal1", + "expected": "aal1", + "credentials": [ + { + "type": "password", + "identifiers": [ + "{email}" + ], + "config": { + "hashed_password": "$2a$fake" + } + } + ] + }, + { + "description": "password without identifier is no credential and ergo aal0", + "expected": "aal0", + "credentials": [ + { + "type": "password", + "config": { + "hashed_password": "$2a$fake" + } + } + ] + }, + { + "description": "second factor totp returns available aal2 even if no password is set", + "expected": "aal2", + "credentials": [ + { + "type": "totp", + "config": { + "totp_url": "totp://" + } + } + ] + }, + { + "description": "second factor totp returns aal0 if totp credentials is not set up", + "expected": "aal0", + "credentials": [ + { + "type": "totp", + "identifiers": [ + "{email}" + ], + "config": {} + } + ] + }, + { + "description": "password and totp is also available aal2", + "expected": "aal1", + "credentials": [ + { + "type": "password", + "identifiers": [ + "{email}" + ], + "config": { + "hashed_password": "$2a$fake" + } + }, + { + "type": "totp", + "identifiers": [ + "{email}" + ], + "config": {} + } + ] + } +] diff --git a/identity/stub/extension/credentials/code-phone-email.schema.json b/identity/stub/extension/credentials/code-phone-email.schema.json new file mode 100644 index 000000000000..4e51a5a0aae8 --- /dev/null +++ b/identity/stub/extension/credentials/code-phone-email.schema.json @@ -0,0 +1,77 @@ +{ + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + } + } + }, + "email2": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + } + } + }, + "email3": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + } + } + } + }, + "phone": { + "type": "string", + "format": "tel", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "sms" + } + } + } + } + } +} diff --git a/identity/stub/extension/credentials/email.schema.json b/identity/stub/extension/credentials/email.schema.json new file mode 100644 index 000000000000..848bfe54c952 --- /dev/null +++ b/identity/stub/extension/credentials/email.schema.json @@ -0,0 +1,44 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/identity.email.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "E-Mail", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "totp": { + "account_name": true + }, + "code": { + "identifier": true, + "via": "email" + }, + "passkey": { + "display_name": true + } + }, + "recovery": { + "via": "email" + }, + "verification": { + "via": "email" + } + }, + "maxLength": 320 + } + }, + "required": [ + "email" + ], + "additionalProperties": false +} diff --git a/identity/test/pool.go b/identity/test/pool.go index c639dd6fc04f..654e25bd9994 100644 --- a/identity/test/pool.go +++ b/identity/test/pool.go @@ -14,6 +14,8 @@ import ( "testing" "time" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/x/crdbx" "github.com/go-faker/faker/v4" @@ -37,12 +39,11 @@ import ( "github.com/ory/x/urlx" ) -func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister, m *identity.Manager, dbname string) func(t *testing.T) { +func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager, dbname string) func(t *testing.T) { return func(t *testing.T) { - exampleServerURL := urlx.ParseOrPanic("http://example.com") - conf.MustSet(ctx, config.ViperKeyPublicBaseURL, exampleServerURL.String()) - nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) + + exampleServerURL := urlx.ParseOrPanic("http://example.com") expandSchema := schema.Schema{ ID: "expandSchema", URL: urlx.ParseOrPanic("file://./stub/expand.schema.json"), @@ -63,22 +64,25 @@ func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister, URL: urlx.ParseOrPanic("file://./stub/handler/multiple_emails.schema.json"), RawURL: "file://./stub/identity-2.schema.json", } - conf.MustSet(ctx, config.ViperKeyIdentitySchemas, []config.Schema{ - { - ID: altSchema.ID, - URL: altSchema.RawURL, - }, - { - ID: defaultSchema.ID, - URL: defaultSchema.RawURL, - }, - { - ID: expandSchema.ID, - URL: expandSchema.RawURL, - }, - { - ID: multipleEmailsSchema.ID, - URL: multipleEmailsSchema.RawURL, + ctx := confighelpers.WithConfigValues(ctx, map[string]any{ + config.ViperKeyPublicBaseURL: exampleServerURL.String(), + config.ViperKeyIdentitySchemas: []config.Schema{ + { + ID: altSchema.ID, + URL: altSchema.RawURL, + }, + { + ID: defaultSchema.ID, + URL: defaultSchema.RawURL, + }, + { + ID: expandSchema.ID, + URL: expandSchema.RawURL, + }, + { + ID: multipleEmailsSchema.ID, + URL: multipleEmailsSchema.RawURL, + }, }, }) @@ -244,15 +248,6 @@ func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister, return i } - webAuthnIdentity := func(schemaID string, credentialsID string) *identity.Identity { - i := identity.NewIdentity(schemaID) - i.SetCredentials(identity.CredentialsTypeWebAuthn, identity.Credentials{ - Type: identity.CredentialsTypeWebAuthn, Identifiers: []string{credentialsID}, - Config: sqlxx.JSONRawMessage(`{"credentials":[{}]}`), - }) - return i - } - oidcIdentity := func(schemaID string, credentialsID string) *identity.Identity { i := identity.NewIdentity(schemaID) i.SetCredentials(identity.CredentialsTypeOIDC, identity.Credentials{ @@ -335,14 +330,14 @@ func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister, t.Run("case=create with null AAL", func(t *testing.T) { expected := passwordIdentity("", "id-"+uuid.Must(uuid.NewV4()).String()) - expected.AvailableAAL.Valid = false + expected.InternalAvailableAAL.Valid = false require.NoError(t, p.CreateIdentity(ctx, expected)) createdIDs = append(createdIDs, expected.ID) actual, err := p.GetIdentity(ctx, expected.ID, identity.ExpandDefault) require.NoError(t, err) - assert.False(t, actual.AvailableAAL.Valid) + assert.False(t, actual.InternalAvailableAAL.Valid) }) t.Run("suite=create multiple identities", func(t *testing.T) { @@ -390,7 +385,14 @@ func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister, }) t.Run("case=run migrations when fetching credentials", func(t *testing.T) { - expected := webAuthnIdentity(altSchema.ID, "webauthn") + expected := func(schemaID string, credentialsID string) *identity.Identity { + i := identity.NewIdentity(schemaID) + i.SetCredentials(identity.CredentialsTypeWebAuthn, identity.Credentials{ + Type: identity.CredentialsTypeWebAuthn, Identifiers: []string{credentialsID}, + Config: sqlxx.JSONRawMessage(`{"credentials":[{}]}`), + }) + return i + }(altSchema.ID, "webauthn") require.NoError(t, p.CreateIdentity(ctx, expected)) createdIDs = append(createdIDs, expected.ID) @@ -567,6 +569,22 @@ func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister, require.Contains(t, err.Error(), "malformed") }) + t.Run("case=update an identity column", func(t *testing.T) { + initial := oidcIdentity("", x.NewUUID().String()) + initial.InternalAvailableAAL = identity.NewNullableAuthenticatorAssuranceLevel(identity.NoAuthenticatorAssuranceLevel) + require.NoError(t, p.CreateIdentity(ctx, initial)) + createdIDs = append(createdIDs, initial.ID) + + initial.InternalAvailableAAL = identity.NewNullableAuthenticatorAssuranceLevel(identity.AuthenticatorAssuranceLevel1) + initial.State = identity.StateInactive + require.NoError(t, p.UpdateIdentityColumns(ctx, initial, "available_aal")) + + actual, err := p.GetIdentity(ctx, initial.ID, identity.ExpandDefault) + require.NoError(t, err) + assert.Equal(t, string(identity.AuthenticatorAssuranceLevel1), actual.InternalAvailableAAL.String) + assert.Equal(t, identity.StateActive, actual.State, "the state remains unchanged") + }) + t.Run("case=should fail to insert identity because credentials from traits exist", func(t *testing.T) { first := passwordIdentity("", "test-identity@ory.sh") first.Traits = identity.Traits(`{}`) @@ -678,10 +696,7 @@ func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister, }) t.Run("list some using ids filter", func(t *testing.T) { - var filterIds []string - for _, id := range createdIDs[:2] { - filterIds = append(filterIds, id.String()) - } + filterIds := createdIDs[:2] is, _, err := p.ListIdentities(ctx, identity.ListIdentityParameters{Expand: identity.ExpandDefault, IdsFilter: filterIds}) require.NoError(t, err) @@ -867,12 +882,59 @@ func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister, // assert.EqualValues(t, expected.Credentials[CredentialsTypePassword].CreatedAt.Unix(), creds.CreatedAt.Unix()) // assert.EqualValues(t, expected.Credentials[CredentialsTypePassword].UpdatedAt.Unix(), creds.UpdatedAt.Unix()) + require.Equal(t, expected.Traits, actual.Traits) + require.Equal(t, expected.ID, actual.ID) + require.NotNil(t, actual.Credentials[identity.CredentialsTypePassword]) + assert.EqualValues(t, expected.Credentials[identity.CredentialsTypePassword].ID, actual.Credentials[identity.CredentialsTypePassword].ID) + assert.EqualValues(t, expected.Credentials[identity.CredentialsTypePassword].Identifiers, actual.Credentials[identity.CredentialsTypePassword].Identifiers) + assert.JSONEq(t, string(expected.Credentials[identity.CredentialsTypePassword].Config), string(actual.Credentials[identity.CredentialsTypePassword].Config)) + + t.Run("not if on another network", func(t *testing.T) { + _, p := testhelpers.NewNetwork(t, ctx, p) + _, _, err := p.FindByCredentialsIdentifier(ctx, identity.CredentialsTypePassword, "find-credentials-identifier@ory.sh") + require.ErrorIs(t, err, sqlcon.ErrNoRows) + }) + }) + + t.Run("case=find identity by its webauthn credential user handle", func(t *testing.T) { + expected := identity.NewIdentity("") + expected.SetCredentials(identity.CredentialsTypeWebAuthn, identity.Credentials{ + Type: identity.CredentialsTypeWebAuthn, + Identifiers: []string{"find-webauth-user-handle-identifier@ory.sh"}, + Config: sqlxx.JSONRawMessage(`{ + "credentials": [ + { + "added_at": "2024-02-13T10:36:16Z", + "attestation_type": "none", + "authenticator": { + "aaguid": "+/wwBxVOTsyMC24CBVfXvQ==", + "clone_warning": false, + "sign_count": 0 + }, + "display_name": "Yubikey", + "id": "f2uGd/Bg1rGcGXtYp4MT4WcN+eA=", + "is_passwordless": true, + "public_key": "pQECAyYgASFYIBkNvUxvjdhuA36FworTmS/rxZR1I+NyRWBpoTYY/R+CIlggw+gFFrFoEi+rS82zq7+tDHAukBUJcFpQ7Z3NLBZH5vk=" + } + ], + "user_handle": "51z80nYJTSGmr6UBe1VGLg==" +}`), + }) + expected.Traits = identity.Traits(`{}`) + userHandle := x.Must(base64.StdEncoding.DecodeString("51z80nYJTSGmr6UBe1VGLg==")) + + require.NoError(t, p.CreateIdentity(ctx, expected)) + createdIDs = append(createdIDs, expected.ID) + + actual, err := p.FindIdentityByWebauthnUserHandle(ctx, userHandle) + require.NoError(t, err) + expected.Credentials = nil assertEqual(t, expected, actual) t.Run("not if on another network", func(t *testing.T) { _, p := testhelpers.NewNetwork(t, ctx, p) - _, _, err := p.FindByCredentialsIdentifier(ctx, identity.CredentialsTypePassword, "find-credentials-identifier@ory.sh") + _, err = p.FindIdentityByWebauthnUserHandle(ctx, userHandle) require.ErrorIs(t, err, sqlcon.ErrNoRows) }) }) @@ -1012,8 +1074,12 @@ func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister, // fandom-end assert.JSONEq(t, string(expected.Credentials[identity.CredentialsTypePassword].Config), string(creds.Config)) - expected.Credentials = nil - assertEqual(t, expected, actual) + require.Equal(t, expected.Traits, actual.Traits) + require.Equal(t, expected.ID, actual.ID) + require.NotNil(t, actual.Credentials[identity.CredentialsTypePassword]) + assert.EqualValues(t, expected.Credentials[identity.CredentialsTypePassword].ID, actual.Credentials[identity.CredentialsTypePassword].ID) + assert.EqualValues(t, []string{strings.ToLower(identifier)}, actual.Credentials[identity.CredentialsTypePassword].Identifiers) + assert.JSONEq(t, string(expected.Credentials[identity.CredentialsTypePassword].Config), string(actual.Credentials[identity.CredentialsTypePassword].Config)) t.Run("not if on another network", func(t *testing.T) { _, p := testhelpers.NewNetwork(t, ctx, p) @@ -1336,7 +1402,7 @@ func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister, i, c, err := p.FindByCredentialsIdentifier(ctx, m[0].Name, "nid1") require.NoError(t, err) assert.Equal(t, "nid1", c.Identifiers[0]) - require.Len(t, i.Credentials, 0) + require.Len(t, i.Credentials, 1) _, _, err = p.FindByCredentialsIdentifier(ctx, m[0].Name, "nid2") require.ErrorIs(t, err, sqlcon.ErrNoRows) diff --git a/identity/validator.go b/identity/validator.go index c2b2c3f03ac7..2e315d75a317 100644 --- a/identity/validator.go +++ b/identity/validator.go @@ -19,7 +19,7 @@ import ( type ( validatorDependencies interface { - IdentityTraitsSchemas(ctx context.Context) (schema.Schemas, error) + schema.IdentitySchemaProvider config.Provider } Validator struct { diff --git a/internal/client-go/.openapi-generator/FILES b/internal/client-go/.openapi-generator/FILES index 01fa0848e5ed..ffb665be9366 100644 --- a/internal/client-go/.openapi-generator/FILES +++ b/internal/client-go/.openapi-generator/FILES @@ -15,12 +15,13 @@ docs/ConsistencyRequestParameters.md docs/ContinueWith.md docs/ContinueWithRecoveryUi.md docs/ContinueWithRecoveryUiFlow.md +docs/ContinueWithRedirectBrowserTo.md docs/ContinueWithSetOrySessionToken.md docs/ContinueWithSettingsUi.md docs/ContinueWithSettingsUiFlow.md docs/ContinueWithVerificationUi.md docs/ContinueWithVerificationUiFlow.md -docs/CourierApi.md +docs/CourierAPI.md docs/CourierMessageStatus.md docs/CourierMessageType.md docs/CreateIdentityBody.md @@ -32,15 +33,16 @@ docs/ErrorBrowserLocationChangeRequired.md docs/ErrorFlowReplaced.md docs/ErrorGeneric.md docs/FlowError.md -docs/FrontendApi.md +docs/FrontendAPI.md docs/GenericError.md docs/GetVersion200Response.md docs/HealthNotReadyStatus.md docs/HealthStatus.md docs/Identity.md -docs/IdentityApi.md +docs/IdentityAPI.md docs/IdentityCredentials.md docs/IdentityCredentialsCode.md +docs/IdentityCredentialsCodeAddress.md docs/IdentityCredentialsOidc.md docs/IdentityCredentialsOidcProvider.md docs/IdentityCredentialsPassword.md @@ -61,7 +63,7 @@ docs/LoginFlowState.md docs/LogoutFlow.md docs/Message.md docs/MessageDispatch.md -docs/MetadataApi.md +docs/MetadataAPI.md docs/NeedsPrivilegedSessionError.md docs/OAuth2Client.md docs/OAuth2ConsentRequestOpenIDConnectContext.md @@ -100,8 +102,10 @@ docs/UiText.md docs/UpdateIdentityBody.md docs/UpdateLoginFlowBody.md docs/UpdateLoginFlowWithCodeMethod.md +docs/UpdateLoginFlowWithIdentifierFirstMethod.md docs/UpdateLoginFlowWithLookupSecretMethod.md docs/UpdateLoginFlowWithOidcMethod.md +docs/UpdateLoginFlowWithPasskeyMethod.md docs/UpdateLoginFlowWithPasswordMethod.md docs/UpdateLoginFlowWithTotpMethod.md docs/UpdateLoginFlowWithWebAuthnMethod.md @@ -111,11 +115,14 @@ docs/UpdateRecoveryFlowWithLinkMethod.md docs/UpdateRegistrationFlowBody.md docs/UpdateRegistrationFlowWithCodeMethod.md docs/UpdateRegistrationFlowWithOidcMethod.md +docs/UpdateRegistrationFlowWithPasskeyMethod.md docs/UpdateRegistrationFlowWithPasswordMethod.md +docs/UpdateRegistrationFlowWithProfileMethod.md docs/UpdateRegistrationFlowWithWebAuthnMethod.md docs/UpdateSettingsFlowBody.md docs/UpdateSettingsFlowWithLookupMethod.md docs/UpdateSettingsFlowWithOidcMethod.md +docs/UpdateSettingsFlowWithPasskeyMethod.md docs/UpdateSettingsFlowWithPasswordMethod.md docs/UpdateSettingsFlowWithProfileMethod.md docs/UpdateSettingsFlowWithTotpMethod.md @@ -136,6 +143,7 @@ model_consistency_request_parameters.go model_continue_with.go model_continue_with_recovery_ui.go model_continue_with_recovery_ui_flow.go +model_continue_with_redirect_browser_to.go model_continue_with_set_ory_session_token.go model_continue_with_settings_ui.go model_continue_with_settings_ui_flow.go @@ -159,6 +167,7 @@ model_health_status.go model_identity.go model_identity_credentials.go model_identity_credentials_code.go +model_identity_credentials_code_address.go model_identity_credentials_oidc.go model_identity_credentials_oidc_provider.go model_identity_credentials_password.go @@ -217,8 +226,10 @@ model_ui_text.go model_update_identity_body.go model_update_login_flow_body.go model_update_login_flow_with_code_method.go +model_update_login_flow_with_identifier_first_method.go model_update_login_flow_with_lookup_secret_method.go model_update_login_flow_with_oidc_method.go +model_update_login_flow_with_passkey_method.go model_update_login_flow_with_password_method.go model_update_login_flow_with_totp_method.go model_update_login_flow_with_web_authn_method.go @@ -228,11 +239,14 @@ model_update_recovery_flow_with_link_method.go model_update_registration_flow_body.go model_update_registration_flow_with_code_method.go model_update_registration_flow_with_oidc_method.go +model_update_registration_flow_with_passkey_method.go model_update_registration_flow_with_password_method.go +model_update_registration_flow_with_profile_method.go model_update_registration_flow_with_web_authn_method.go model_update_settings_flow_body.go model_update_settings_flow_with_lookup_method.go model_update_settings_flow_with_oidc_method.go +model_update_settings_flow_with_passkey_method.go model_update_settings_flow_with_password_method.go model_update_settings_flow_with_profile_method.go model_update_settings_flow_with_totp_method.go diff --git a/internal/client-go/README.md b/internal/client-go/README.md index b31d69c86354..d0256bfaec2b 100644 --- a/internal/client-go/README.md +++ b/internal/client-go/README.md @@ -6,7 +6,7 @@ This is the API specification for Ory Identities with features such as registrat ## Overview This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [OpenAPI-spec](https://www.openapis.org/) from a remote server, you can easily generate an API client. -- API version: +- API version: - Package version: 1.0.0 - Build package: org.openapitools.codegen.languages.GoClientCodegen @@ -79,63 +79,59 @@ All URIs are relative to *http://localhost* Class | Method | HTTP request | Description ------------ | ------------- | ------------- | ------------- -*CourierApi* | [**GetCourierMessage**](docs/CourierApi.md#getcouriermessage) | **Get** /admin/courier/messages/{id} | Get a Message -*CourierApi* | [**ListCourierMessages**](docs/CourierApi.md#listcouriermessages) | **Get** /admin/courier/messages | List Messages -*FrontendApi* | [**CreateBrowserLoginFlow**](docs/FrontendApi.md#createbrowserloginflow) | **Get** /self-service/login/browser | Create Login Flow for Browsers -*FrontendApi* | [**CreateBrowserLogoutFlow**](docs/FrontendApi.md#createbrowserlogoutflow) | **Get** /self-service/logout/browser | Create a Logout URL for Browsers -*FrontendApi* | [**CreateBrowserRecoveryFlow**](docs/FrontendApi.md#createbrowserrecoveryflow) | **Get** /self-service/recovery/browser | Create Recovery Flow for Browsers -*FrontendApi* | [**CreateBrowserRegistrationFlow**](docs/FrontendApi.md#createbrowserregistrationflow) | **Get** /self-service/registration/browser | Create Registration Flow for Browsers -*FrontendApi* | [**CreateBrowserSettingsFlow**](docs/FrontendApi.md#createbrowsersettingsflow) | **Get** /self-service/settings/browser | Create Settings Flow for Browsers -*FrontendApi* | [**CreateBrowserVerificationFlow**](docs/FrontendApi.md#createbrowserverificationflow) | **Get** /self-service/verification/browser | Create Verification Flow for Browser Clients -*FrontendApi* | [**CreateNativeLoginFlow**](docs/FrontendApi.md#createnativeloginflow) | **Get** /self-service/login/api | Create Login Flow for Native Apps -*FrontendApi* | [**CreateNativeRecoveryFlow**](docs/FrontendApi.md#createnativerecoveryflow) | **Get** /self-service/recovery/api | Create Recovery Flow for Native Apps -*FrontendApi* | [**CreateNativeRegistrationFlow**](docs/FrontendApi.md#createnativeregistrationflow) | **Get** /self-service/registration/api | Create Registration Flow for Native Apps -*FrontendApi* | [**CreateNativeSettingsFlow**](docs/FrontendApi.md#createnativesettingsflow) | **Get** /self-service/settings/api | Create Settings Flow for Native Apps -*FrontendApi* | [**CreateNativeVerificationFlow**](docs/FrontendApi.md#createnativeverificationflow) | **Get** /self-service/verification/api | Create Verification Flow for Native Apps -*FrontendApi* | [**DisableMyOtherSessions**](docs/FrontendApi.md#disablemyothersessions) | **Delete** /sessions | Disable my other sessions -*FrontendApi* | [**DisableMySession**](docs/FrontendApi.md#disablemysession) | **Delete** /sessions/{id} | Disable one of my sessions -*FrontendApi* | [**ExchangeSessionToken**](docs/FrontendApi.md#exchangesessiontoken) | **Get** /sessions/token-exchange | Exchange Session Token -*FrontendApi* | [**GetFlowError**](docs/FrontendApi.md#getflowerror) | **Get** /self-service/errors | Get User-Flow Errors -*FrontendApi* | [**GetLoginFlow**](docs/FrontendApi.md#getloginflow) | **Get** /self-service/login/flows | Get Login Flow -*FrontendApi* | [**GetRecoveryFlow**](docs/FrontendApi.md#getrecoveryflow) | **Get** /self-service/recovery/flows | Get Recovery Flow -*FrontendApi* | [**GetRegistrationFlow**](docs/FrontendApi.md#getregistrationflow) | **Get** /self-service/registration/flows | Get Registration Flow -*FrontendApi* | [**GetSettingsFlow**](docs/FrontendApi.md#getsettingsflow) | **Get** /self-service/settings/flows | Get Settings Flow -*FrontendApi* | [**GetVerificationFlow**](docs/FrontendApi.md#getverificationflow) | **Get** /self-service/verification/flows | Get Verification Flow -*FrontendApi* | [**GetWebAuthnJavaScript**](docs/FrontendApi.md#getwebauthnjavascript) | **Get** /.well-known/ory/webauthn.js | Get WebAuthn JavaScript -*FrontendApi* | [**ListMySessions**](docs/FrontendApi.md#listmysessions) | **Get** /sessions | Get My Active Sessions -*FrontendApi* | [**PerformNativeLogout**](docs/FrontendApi.md#performnativelogout) | **Delete** /self-service/logout/api | Perform Logout for Native Apps -*FrontendApi* | [**ToSession**](docs/FrontendApi.md#tosession) | **Get** /sessions/whoami | Check Who the Current HTTP Session Belongs To -*FrontendApi* | [**UpdateLoginFlow**](docs/FrontendApi.md#updateloginflow) | **Post** /self-service/login | Submit a Login Flow -*FrontendApi* | [**UpdateLogoutFlow**](docs/FrontendApi.md#updatelogoutflow) | **Get** /self-service/logout | Update Logout Flow -*FrontendApi* | [**UpdateRecoveryFlow**](docs/FrontendApi.md#updaterecoveryflow) | **Post** /self-service/recovery | Update Recovery Flow -*FrontendApi* | [**UpdateRegistrationFlow**](docs/FrontendApi.md#updateregistrationflow) | **Post** /self-service/registration | Update Registration Flow -*FrontendApi* | [**UpdateSettingsFlow**](docs/FrontendApi.md#updatesettingsflow) | **Post** /self-service/settings | Complete Settings Flow -*FrontendApi* | [**UpdateVerificationFlow**](docs/FrontendApi.md#updateverificationflow) | **Post** /self-service/verification | Complete Verification Flow -*IdentityApi* | [**AdminCurrentSessionExtend**](docs/IdentityApi.md#admincurrentsessionextend) | **Get** /admin/token/extend | Calling this endpoint refreshes a current user session. If `session.refresh_min_time_left` is set it will only refresh the session after this time has passed. -*IdentityApi* | [**AdminIdentitySession**](docs/IdentityApi.md#adminidentitysession) | **Get** /admin/identities/{id}/session | Calling this endpoint issues a session for a given identity. -*IdentityApi* | [**AdminUpdateCredentials**](docs/IdentityApi.md#adminupdatecredentials) | **Put** /identities/{id}/credentials | Update Identity Credentials -*IdentityApi* | [**AdminUpdateIdentityBody**](docs/IdentityApi.md#adminupdateidentitybody) | **Post** /identities/validate | Validates provided traits and state -*IdentityApi* | [**BatchPatchIdentities**](docs/IdentityApi.md#batchpatchidentities) | **Patch** /admin/identities | Create and deletes multiple identities -*IdentityApi* | [**CreateIdentity**](docs/IdentityApi.md#createidentity) | **Post** /admin/identities | Create an Identity -*IdentityApi* | [**CreateRecoveryCodeForIdentity**](docs/IdentityApi.md#createrecoverycodeforidentity) | **Post** /admin/recovery/code | Create a Recovery Code -*IdentityApi* | [**CreateRecoveryLinkForIdentity**](docs/IdentityApi.md#createrecoverylinkforidentity) | **Post** /admin/recovery/link | Create a Recovery Link -*IdentityApi* | [**DeleteIdentity**](docs/IdentityApi.md#deleteidentity) | **Delete** /admin/identities/{id} | Delete an Identity -*IdentityApi* | [**DeleteIdentityCredentials**](docs/IdentityApi.md#deleteidentitycredentials) | **Delete** /admin/identities/{id}/credentials/{type} | Delete a credential for a specific identity -*IdentityApi* | [**DeleteIdentitySessions**](docs/IdentityApi.md#deleteidentitysessions) | **Delete** /admin/identities/{id}/sessions | Delete & Invalidate an Identity's Sessions -*IdentityApi* | [**DisableSession**](docs/IdentityApi.md#disablesession) | **Delete** /admin/sessions/{id} | Deactivate a Session -*IdentityApi* | [**ExtendSession**](docs/IdentityApi.md#extendsession) | **Patch** /admin/sessions/{id}/extend | Extend a Session -*IdentityApi* | [**GetIdentity**](docs/IdentityApi.md#getidentity) | **Get** /admin/identities/{id} | Get an Identity -*IdentityApi* | [**GetIdentitySchema**](docs/IdentityApi.md#getidentityschema) | **Get** /schemas/{id} | Get Identity JSON Schema -*IdentityApi* | [**GetSession**](docs/IdentityApi.md#getsession) | **Get** /admin/sessions/{id} | Get Session -*IdentityApi* | [**ListIdentities**](docs/IdentityApi.md#listidentities) | **Get** /admin/identities | List Identities -*IdentityApi* | [**ListIdentitySchemas**](docs/IdentityApi.md#listidentityschemas) | **Get** /schemas | Get all Identity Schemas -*IdentityApi* | [**ListIdentitySessions**](docs/IdentityApi.md#listidentitysessions) | **Get** /admin/identities/{id}/sessions | List an Identity's Sessions -*IdentityApi* | [**ListSessions**](docs/IdentityApi.md#listsessions) | **Get** /admin/sessions | List All Sessions -*IdentityApi* | [**PatchIdentity**](docs/IdentityApi.md#patchidentity) | **Patch** /admin/identities/{id} | Patch an Identity -*IdentityApi* | [**UpdateIdentity**](docs/IdentityApi.md#updateidentity) | **Put** /admin/identities/{id} | Update an Identity -*MetadataApi* | [**GetVersion**](docs/MetadataApi.md#getversion) | **Get** /version | Return Running Software Version. -*MetadataApi* | [**IsAlive**](docs/MetadataApi.md#isalive) | **Get** /health/alive | Check HTTP Server Status -*MetadataApi* | [**IsReady**](docs/MetadataApi.md#isready) | **Get** /health/ready | Check HTTP Server and Database Status +*CourierAPI* | [**GetCourierMessage**](docs/CourierAPI.md#getcouriermessage) | **Get** /admin/courier/messages/{id} | Get a Message +*CourierAPI* | [**ListCourierMessages**](docs/CourierAPI.md#listcouriermessages) | **Get** /admin/courier/messages | List Messages +*FrontendAPI* | [**CreateBrowserLoginFlow**](docs/FrontendAPI.md#createbrowserloginflow) | **Get** /self-service/login/browser | Create Login Flow for Browsers +*FrontendAPI* | [**CreateBrowserLogoutFlow**](docs/FrontendAPI.md#createbrowserlogoutflow) | **Get** /self-service/logout/browser | Create a Logout URL for Browsers +*FrontendAPI* | [**CreateBrowserRecoveryFlow**](docs/FrontendAPI.md#createbrowserrecoveryflow) | **Get** /self-service/recovery/browser | Create Recovery Flow for Browsers +*FrontendAPI* | [**CreateBrowserRegistrationFlow**](docs/FrontendAPI.md#createbrowserregistrationflow) | **Get** /self-service/registration/browser | Create Registration Flow for Browsers +*FrontendAPI* | [**CreateBrowserSettingsFlow**](docs/FrontendAPI.md#createbrowsersettingsflow) | **Get** /self-service/settings/browser | Create Settings Flow for Browsers +*FrontendAPI* | [**CreateBrowserVerificationFlow**](docs/FrontendAPI.md#createbrowserverificationflow) | **Get** /self-service/verification/browser | Create Verification Flow for Browser Clients +*FrontendAPI* | [**CreateNativeLoginFlow**](docs/FrontendAPI.md#createnativeloginflow) | **Get** /self-service/login/api | Create Login Flow for Native Apps +*FrontendAPI* | [**CreateNativeRecoveryFlow**](docs/FrontendAPI.md#createnativerecoveryflow) | **Get** /self-service/recovery/api | Create Recovery Flow for Native Apps +*FrontendAPI* | [**CreateNativeRegistrationFlow**](docs/FrontendAPI.md#createnativeregistrationflow) | **Get** /self-service/registration/api | Create Registration Flow for Native Apps +*FrontendAPI* | [**CreateNativeSettingsFlow**](docs/FrontendAPI.md#createnativesettingsflow) | **Get** /self-service/settings/api | Create Settings Flow for Native Apps +*FrontendAPI* | [**CreateNativeVerificationFlow**](docs/FrontendAPI.md#createnativeverificationflow) | **Get** /self-service/verification/api | Create Verification Flow for Native Apps +*FrontendAPI* | [**DisableMyOtherSessions**](docs/FrontendAPI.md#disablemyothersessions) | **Delete** /sessions | Disable my other sessions +*FrontendAPI* | [**DisableMySession**](docs/FrontendAPI.md#disablemysession) | **Delete** /sessions/{id} | Disable one of my sessions +*FrontendAPI* | [**ExchangeSessionToken**](docs/FrontendAPI.md#exchangesessiontoken) | **Get** /sessions/token-exchange | Exchange Session Token +*FrontendAPI* | [**GetFlowError**](docs/FrontendAPI.md#getflowerror) | **Get** /self-service/errors | Get User-Flow Errors +*FrontendAPI* | [**GetLoginFlow**](docs/FrontendAPI.md#getloginflow) | **Get** /self-service/login/flows | Get Login Flow +*FrontendAPI* | [**GetRecoveryFlow**](docs/FrontendAPI.md#getrecoveryflow) | **Get** /self-service/recovery/flows | Get Recovery Flow +*FrontendAPI* | [**GetRegistrationFlow**](docs/FrontendAPI.md#getregistrationflow) | **Get** /self-service/registration/flows | Get Registration Flow +*FrontendAPI* | [**GetSettingsFlow**](docs/FrontendAPI.md#getsettingsflow) | **Get** /self-service/settings/flows | Get Settings Flow +*FrontendAPI* | [**GetVerificationFlow**](docs/FrontendAPI.md#getverificationflow) | **Get** /self-service/verification/flows | Get Verification Flow +*FrontendAPI* | [**GetWebAuthnJavaScript**](docs/FrontendAPI.md#getwebauthnjavascript) | **Get** /.well-known/ory/webauthn.js | Get WebAuthn JavaScript +*FrontendAPI* | [**ListMySessions**](docs/FrontendAPI.md#listmysessions) | **Get** /sessions | Get My Active Sessions +*FrontendAPI* | [**PerformNativeLogout**](docs/FrontendAPI.md#performnativelogout) | **Delete** /self-service/logout/api | Perform Logout for Native Apps +*FrontendAPI* | [**ToSession**](docs/FrontendAPI.md#tosession) | **Get** /sessions/whoami | Check Who the Current HTTP Session Belongs To +*FrontendAPI* | [**UpdateLoginFlow**](docs/FrontendAPI.md#updateloginflow) | **Post** /self-service/login | Submit a Login Flow +*FrontendAPI* | [**UpdateLogoutFlow**](docs/FrontendAPI.md#updatelogoutflow) | **Get** /self-service/logout | Update Logout Flow +*FrontendAPI* | [**UpdateRecoveryFlow**](docs/FrontendAPI.md#updaterecoveryflow) | **Post** /self-service/recovery | Update Recovery Flow +*FrontendAPI* | [**UpdateRegistrationFlow**](docs/FrontendAPI.md#updateregistrationflow) | **Post** /self-service/registration | Update Registration Flow +*FrontendAPI* | [**UpdateSettingsFlow**](docs/FrontendAPI.md#updatesettingsflow) | **Post** /self-service/settings | Complete Settings Flow +*FrontendAPI* | [**UpdateVerificationFlow**](docs/FrontendAPI.md#updateverificationflow) | **Post** /self-service/verification | Complete Verification Flow +*IdentityAPI* | [**BatchPatchIdentities**](docs/IdentityAPI.md#batchpatchidentities) | **Patch** /admin/identities | Create multiple identities +*IdentityAPI* | [**CreateIdentity**](docs/IdentityAPI.md#createidentity) | **Post** /admin/identities | Create an Identity +*IdentityAPI* | [**CreateRecoveryCodeForIdentity**](docs/IdentityAPI.md#createrecoverycodeforidentity) | **Post** /admin/recovery/code | Create a Recovery Code +*IdentityAPI* | [**CreateRecoveryLinkForIdentity**](docs/IdentityAPI.md#createrecoverylinkforidentity) | **Post** /admin/recovery/link | Create a Recovery Link +*IdentityAPI* | [**DeleteIdentity**](docs/IdentityAPI.md#deleteidentity) | **Delete** /admin/identities/{id} | Delete an Identity +*IdentityAPI* | [**DeleteIdentityCredentials**](docs/IdentityAPI.md#deleteidentitycredentials) | **Delete** /admin/identities/{id}/credentials/{type} | Delete a credential for a specific identity +*IdentityAPI* | [**DeleteIdentitySessions**](docs/IdentityAPI.md#deleteidentitysessions) | **Delete** /admin/identities/{id}/sessions | Delete & Invalidate an Identity's Sessions +*IdentityAPI* | [**DisableSession**](docs/IdentityAPI.md#disablesession) | **Delete** /admin/sessions/{id} | Deactivate a Session +*IdentityAPI* | [**ExtendSession**](docs/IdentityAPI.md#extendsession) | **Patch** /admin/sessions/{id}/extend | Extend a Session +*IdentityAPI* | [**GetIdentity**](docs/IdentityAPI.md#getidentity) | **Get** /admin/identities/{id} | Get an Identity +*IdentityAPI* | [**GetIdentitySchema**](docs/IdentityAPI.md#getidentityschema) | **Get** /schemas/{id} | Get Identity JSON Schema +*IdentityAPI* | [**GetSession**](docs/IdentityAPI.md#getsession) | **Get** /admin/sessions/{id} | Get Session +*IdentityAPI* | [**ListIdentities**](docs/IdentityAPI.md#listidentities) | **Get** /admin/identities | List Identities +*IdentityAPI* | [**ListIdentitySchemas**](docs/IdentityAPI.md#listidentityschemas) | **Get** /schemas | Get all Identity Schemas +*IdentityAPI* | [**ListIdentitySessions**](docs/IdentityAPI.md#listidentitysessions) | **Get** /admin/identities/{id}/sessions | List an Identity's Sessions +*IdentityAPI* | [**ListSessions**](docs/IdentityAPI.md#listsessions) | **Get** /admin/sessions | List All Sessions +*IdentityAPI* | [**PatchIdentity**](docs/IdentityAPI.md#patchidentity) | **Patch** /admin/identities/{id} | Patch an Identity +*IdentityAPI* | [**UpdateIdentity**](docs/IdentityAPI.md#updateidentity) | **Put** /admin/identities/{id} | Update an Identity +*MetadataAPI* | [**GetVersion**](docs/MetadataAPI.md#getversion) | **Get** /version | Return Running Software Version. +*MetadataAPI* | [**IsAlive**](docs/MetadataAPI.md#isalive) | **Get** /health/alive | Check HTTP Server Status +*MetadataAPI* | [**IsReady**](docs/MetadataAPI.md#isready) | **Get** /health/ready | Check HTTP Server and Database Status ## Documentation For Models @@ -146,6 +142,7 @@ Class | Method | HTTP request | Description - [ContinueWith](docs/ContinueWith.md) - [ContinueWithRecoveryUi](docs/ContinueWithRecoveryUi.md) - [ContinueWithRecoveryUiFlow](docs/ContinueWithRecoveryUiFlow.md) + - [ContinueWithRedirectBrowserTo](docs/ContinueWithRedirectBrowserTo.md) - [ContinueWithSetOrySessionToken](docs/ContinueWithSetOrySessionToken.md) - [ContinueWithSettingsUi](docs/ContinueWithSettingsUi.md) - [ContinueWithSettingsUiFlow](docs/ContinueWithSettingsUiFlow.md) @@ -169,6 +166,7 @@ Class | Method | HTTP request | Description - [Identity](docs/Identity.md) - [IdentityCredentials](docs/IdentityCredentials.md) - [IdentityCredentialsCode](docs/IdentityCredentialsCode.md) + - [IdentityCredentialsCodeAddress](docs/IdentityCredentialsCodeAddress.md) - [IdentityCredentialsOidc](docs/IdentityCredentialsOidc.md) - [IdentityCredentialsOidcProvider](docs/IdentityCredentialsOidcProvider.md) - [IdentityCredentialsPassword](docs/IdentityCredentialsPassword.md) @@ -227,8 +225,10 @@ Class | Method | HTTP request | Description - [UpdateIdentityBody](docs/UpdateIdentityBody.md) - [UpdateLoginFlowBody](docs/UpdateLoginFlowBody.md) - [UpdateLoginFlowWithCodeMethod](docs/UpdateLoginFlowWithCodeMethod.md) + - [UpdateLoginFlowWithIdentifierFirstMethod](docs/UpdateLoginFlowWithIdentifierFirstMethod.md) - [UpdateLoginFlowWithLookupSecretMethod](docs/UpdateLoginFlowWithLookupSecretMethod.md) - [UpdateLoginFlowWithOidcMethod](docs/UpdateLoginFlowWithOidcMethod.md) + - [UpdateLoginFlowWithPasskeyMethod](docs/UpdateLoginFlowWithPasskeyMethod.md) - [UpdateLoginFlowWithPasswordMethod](docs/UpdateLoginFlowWithPasswordMethod.md) - [UpdateLoginFlowWithTotpMethod](docs/UpdateLoginFlowWithTotpMethod.md) - [UpdateLoginFlowWithWebAuthnMethod](docs/UpdateLoginFlowWithWebAuthnMethod.md) @@ -238,11 +238,14 @@ Class | Method | HTTP request | Description - [UpdateRegistrationFlowBody](docs/UpdateRegistrationFlowBody.md) - [UpdateRegistrationFlowWithCodeMethod](docs/UpdateRegistrationFlowWithCodeMethod.md) - [UpdateRegistrationFlowWithOidcMethod](docs/UpdateRegistrationFlowWithOidcMethod.md) + - [UpdateRegistrationFlowWithPasskeyMethod](docs/UpdateRegistrationFlowWithPasskeyMethod.md) - [UpdateRegistrationFlowWithPasswordMethod](docs/UpdateRegistrationFlowWithPasswordMethod.md) + - [UpdateRegistrationFlowWithProfileMethod](docs/UpdateRegistrationFlowWithProfileMethod.md) - [UpdateRegistrationFlowWithWebAuthnMethod](docs/UpdateRegistrationFlowWithWebAuthnMethod.md) - [UpdateSettingsFlowBody](docs/UpdateSettingsFlowBody.md) - [UpdateSettingsFlowWithLookupMethod](docs/UpdateSettingsFlowWithLookupMethod.md) - [UpdateSettingsFlowWithOidcMethod](docs/UpdateSettingsFlowWithOidcMethod.md) + - [UpdateSettingsFlowWithPasskeyMethod](docs/UpdateSettingsFlowWithPasskeyMethod.md) - [UpdateSettingsFlowWithPasswordMethod](docs/UpdateSettingsFlowWithPasswordMethod.md) - [UpdateSettingsFlowWithProfileMethod](docs/UpdateSettingsFlowWithProfileMethod.md) - [UpdateSettingsFlowWithTotpMethod](docs/UpdateSettingsFlowWithTotpMethod.md) diff --git a/internal/client-go/api_courier.go b/internal/client-go/api_courier.go index a7e43bcd183e..36f10d0a6281 100644 --- a/internal/client-go/api_courier.go +++ b/internal/client-go/api_courier.go @@ -25,48 +25,48 @@ var ( _ context.Context ) -type CourierApi interface { +type CourierAPI interface { /* * GetCourierMessage Get a Message * Gets a specific messages by the given ID. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id MessageID is the ID of the message. - * @return CourierApiApiGetCourierMessageRequest + * @return CourierAPIApiGetCourierMessageRequest */ - GetCourierMessage(ctx context.Context, id string) CourierApiApiGetCourierMessageRequest + GetCourierMessage(ctx context.Context, id string) CourierAPIApiGetCourierMessageRequest /* * GetCourierMessageExecute executes the request * @return Message */ - GetCourierMessageExecute(r CourierApiApiGetCourierMessageRequest) (*Message, *http.Response, error) + GetCourierMessageExecute(r CourierAPIApiGetCourierMessageRequest) (*Message, *http.Response, error) /* * ListCourierMessages List Messages * Lists all messages by given status and recipient. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return CourierApiApiListCourierMessagesRequest + * @return CourierAPIApiListCourierMessagesRequest */ - ListCourierMessages(ctx context.Context) CourierApiApiListCourierMessagesRequest + ListCourierMessages(ctx context.Context) CourierAPIApiListCourierMessagesRequest /* * ListCourierMessagesExecute executes the request * @return []Message */ - ListCourierMessagesExecute(r CourierApiApiListCourierMessagesRequest) ([]Message, *http.Response, error) + ListCourierMessagesExecute(r CourierAPIApiListCourierMessagesRequest) ([]Message, *http.Response, error) } -// CourierApiService CourierApi service -type CourierApiService service +// CourierAPIService CourierAPI service +type CourierAPIService service -type CourierApiApiGetCourierMessageRequest struct { +type CourierAPIApiGetCourierMessageRequest struct { ctx context.Context - ApiService CourierApi + ApiService CourierAPI id string } -func (r CourierApiApiGetCourierMessageRequest) Execute() (*Message, *http.Response, error) { +func (r CourierAPIApiGetCourierMessageRequest) Execute() (*Message, *http.Response, error) { return r.ApiService.GetCourierMessageExecute(r) } @@ -75,10 +75,10 @@ func (r CourierApiApiGetCourierMessageRequest) Execute() (*Message, *http.Respon * Gets a specific messages by the given ID. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id MessageID is the ID of the message. - * @return CourierApiApiGetCourierMessageRequest + * @return CourierAPIApiGetCourierMessageRequest */ -func (a *CourierApiService) GetCourierMessage(ctx context.Context, id string) CourierApiApiGetCourierMessageRequest { - return CourierApiApiGetCourierMessageRequest{ +func (a *CourierAPIService) GetCourierMessage(ctx context.Context, id string) CourierAPIApiGetCourierMessageRequest { + return CourierAPIApiGetCourierMessageRequest{ ApiService: a, ctx: ctx, id: id, @@ -89,7 +89,7 @@ func (a *CourierApiService) GetCourierMessage(ctx context.Context, id string) Co * Execute executes the request * @return Message */ -func (a *CourierApiService) GetCourierMessageExecute(r CourierApiApiGetCourierMessageRequest) (*Message, *http.Response, error) { +func (a *CourierAPIService) GetCourierMessageExecute(r CourierAPIApiGetCourierMessageRequest) (*Message, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -99,7 +99,7 @@ func (a *CourierApiService) GetCourierMessageExecute(r CourierApiApiGetCourierMe localVarReturnValue *Message ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CourierApiService.GetCourierMessage") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CourierAPIService.GetCourierMessage") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -152,7 +152,7 @@ func (a *CourierApiService) GetCourierMessageExecute(r CourierApiApiGetCourierMe return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -196,33 +196,33 @@ func (a *CourierApiService) GetCourierMessageExecute(r CourierApiApiGetCourierMe return localVarReturnValue, localVarHTTPResponse, nil } -type CourierApiApiListCourierMessagesRequest struct { +type CourierAPIApiListCourierMessagesRequest struct { ctx context.Context - ApiService CourierApi + ApiService CourierAPI pageSize *int64 pageToken *string status *CourierMessageStatus recipient *string } -func (r CourierApiApiListCourierMessagesRequest) PageSize(pageSize int64) CourierApiApiListCourierMessagesRequest { +func (r CourierAPIApiListCourierMessagesRequest) PageSize(pageSize int64) CourierAPIApiListCourierMessagesRequest { r.pageSize = &pageSize return r } -func (r CourierApiApiListCourierMessagesRequest) PageToken(pageToken string) CourierApiApiListCourierMessagesRequest { +func (r CourierAPIApiListCourierMessagesRequest) PageToken(pageToken string) CourierAPIApiListCourierMessagesRequest { r.pageToken = &pageToken return r } -func (r CourierApiApiListCourierMessagesRequest) Status(status CourierMessageStatus) CourierApiApiListCourierMessagesRequest { +func (r CourierAPIApiListCourierMessagesRequest) Status(status CourierMessageStatus) CourierAPIApiListCourierMessagesRequest { r.status = &status return r } -func (r CourierApiApiListCourierMessagesRequest) Recipient(recipient string) CourierApiApiListCourierMessagesRequest { +func (r CourierAPIApiListCourierMessagesRequest) Recipient(recipient string) CourierAPIApiListCourierMessagesRequest { r.recipient = &recipient return r } -func (r CourierApiApiListCourierMessagesRequest) Execute() ([]Message, *http.Response, error) { +func (r CourierAPIApiListCourierMessagesRequest) Execute() ([]Message, *http.Response, error) { return r.ApiService.ListCourierMessagesExecute(r) } @@ -230,10 +230,10 @@ func (r CourierApiApiListCourierMessagesRequest) Execute() ([]Message, *http.Res * ListCourierMessages List Messages * Lists all messages by given status and recipient. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return CourierApiApiListCourierMessagesRequest + * @return CourierAPIApiListCourierMessagesRequest */ -func (a *CourierApiService) ListCourierMessages(ctx context.Context) CourierApiApiListCourierMessagesRequest { - return CourierApiApiListCourierMessagesRequest{ +func (a *CourierAPIService) ListCourierMessages(ctx context.Context) CourierAPIApiListCourierMessagesRequest { + return CourierAPIApiListCourierMessagesRequest{ ApiService: a, ctx: ctx, } @@ -243,7 +243,7 @@ func (a *CourierApiService) ListCourierMessages(ctx context.Context) CourierApiA * Execute executes the request * @return []Message */ -func (a *CourierApiService) ListCourierMessagesExecute(r CourierApiApiListCourierMessagesRequest) ([]Message, *http.Response, error) { +func (a *CourierAPIService) ListCourierMessagesExecute(r CourierAPIApiListCourierMessagesRequest) ([]Message, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -253,7 +253,7 @@ func (a *CourierApiService) ListCourierMessagesExecute(r CourierApiApiListCourie localVarReturnValue []Message ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CourierApiService.ListCourierMessages") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CourierAPIService.ListCourierMessages") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -317,7 +317,7 @@ func (a *CourierApiService) ListCourierMessagesExecute(r CourierApiApiListCourie return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { diff --git a/internal/client-go/api_frontend.go b/internal/client-go/api_frontend.go index cc5f2531a640..94dc1ad0e152 100644 --- a/internal/client-go/api_frontend.go +++ b/internal/client-go/api_frontend.go @@ -25,7 +25,7 @@ var ( _ context.Context ) -type FrontendApi interface { +type FrontendAPI interface { /* * CreateBrowserLoginFlow Create Login Flow for Browsers @@ -53,15 +53,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserLoginFlowRequest + * @return FrontendAPIApiCreateBrowserLoginFlowRequest */ - CreateBrowserLoginFlow(ctx context.Context) FrontendApiApiCreateBrowserLoginFlowRequest + CreateBrowserLoginFlow(ctx context.Context) FrontendAPIApiCreateBrowserLoginFlowRequest /* * CreateBrowserLoginFlowExecute executes the request * @return LoginFlow */ - CreateBrowserLoginFlowExecute(r FrontendApiApiCreateBrowserLoginFlowRequest) (*LoginFlow, *http.Response, error) + CreateBrowserLoginFlowExecute(r FrontendAPIApiCreateBrowserLoginFlowRequest) (*LoginFlow, *http.Response, error) /* * CreateBrowserLogoutFlow Create a Logout URL for Browsers @@ -76,15 +76,15 @@ type FrontendApi interface { When calling this endpoint from a backend, please ensure to properly forward the HTTP cookies. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserLogoutFlowRequest + * @return FrontendAPIApiCreateBrowserLogoutFlowRequest */ - CreateBrowserLogoutFlow(ctx context.Context) FrontendApiApiCreateBrowserLogoutFlowRequest + CreateBrowserLogoutFlow(ctx context.Context) FrontendAPIApiCreateBrowserLogoutFlowRequest /* * CreateBrowserLogoutFlowExecute executes the request * @return LogoutFlow */ - CreateBrowserLogoutFlowExecute(r FrontendApiApiCreateBrowserLogoutFlowRequest) (*LogoutFlow, *http.Response, error) + CreateBrowserLogoutFlowExecute(r FrontendAPIApiCreateBrowserLogoutFlowRequest) (*LogoutFlow, *http.Response, error) /* * CreateBrowserRecoveryFlow Create Recovery Flow for Browsers @@ -99,15 +99,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserRecoveryFlowRequest + * @return FrontendAPIApiCreateBrowserRecoveryFlowRequest */ - CreateBrowserRecoveryFlow(ctx context.Context) FrontendApiApiCreateBrowserRecoveryFlowRequest + CreateBrowserRecoveryFlow(ctx context.Context) FrontendAPIApiCreateBrowserRecoveryFlowRequest /* * CreateBrowserRecoveryFlowExecute executes the request * @return RecoveryFlow */ - CreateBrowserRecoveryFlowExecute(r FrontendApiApiCreateBrowserRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) + CreateBrowserRecoveryFlowExecute(r FrontendAPIApiCreateBrowserRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) /* * CreateBrowserRegistrationFlow Create Registration Flow for Browsers @@ -131,15 +131,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserRegistrationFlowRequest + * @return FrontendAPIApiCreateBrowserRegistrationFlowRequest */ - CreateBrowserRegistrationFlow(ctx context.Context) FrontendApiApiCreateBrowserRegistrationFlowRequest + CreateBrowserRegistrationFlow(ctx context.Context) FrontendAPIApiCreateBrowserRegistrationFlowRequest /* * CreateBrowserRegistrationFlowExecute executes the request * @return RegistrationFlow */ - CreateBrowserRegistrationFlowExecute(r FrontendApiApiCreateBrowserRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) + CreateBrowserRegistrationFlowExecute(r FrontendAPIApiCreateBrowserRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) /* * CreateBrowserSettingsFlow Create Settings Flow for Browsers @@ -170,15 +170,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserSettingsFlowRequest + * @return FrontendAPIApiCreateBrowserSettingsFlowRequest */ - CreateBrowserSettingsFlow(ctx context.Context) FrontendApiApiCreateBrowserSettingsFlowRequest + CreateBrowserSettingsFlow(ctx context.Context) FrontendAPIApiCreateBrowserSettingsFlowRequest /* * CreateBrowserSettingsFlowExecute executes the request * @return SettingsFlow */ - CreateBrowserSettingsFlowExecute(r FrontendApiApiCreateBrowserSettingsFlowRequest) (*SettingsFlow, *http.Response, error) + CreateBrowserSettingsFlowExecute(r FrontendAPIApiCreateBrowserSettingsFlowRequest) (*SettingsFlow, *http.Response, error) /* * CreateBrowserVerificationFlow Create Verification Flow for Browser Clients @@ -191,15 +191,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserVerificationFlowRequest + * @return FrontendAPIApiCreateBrowserVerificationFlowRequest */ - CreateBrowserVerificationFlow(ctx context.Context) FrontendApiApiCreateBrowserVerificationFlowRequest + CreateBrowserVerificationFlow(ctx context.Context) FrontendAPIApiCreateBrowserVerificationFlowRequest /* * CreateBrowserVerificationFlowExecute executes the request * @return VerificationFlow */ - CreateBrowserVerificationFlowExecute(r FrontendApiApiCreateBrowserVerificationFlowRequest) (*VerificationFlow, *http.Response, error) + CreateBrowserVerificationFlowExecute(r FrontendAPIApiCreateBrowserVerificationFlowRequest) (*VerificationFlow, *http.Response, error) /* * CreateNativeLoginFlow Create Login Flow for Native Apps @@ -224,15 +224,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeLoginFlowRequest + * @return FrontendAPIApiCreateNativeLoginFlowRequest */ - CreateNativeLoginFlow(ctx context.Context) FrontendApiApiCreateNativeLoginFlowRequest + CreateNativeLoginFlow(ctx context.Context) FrontendAPIApiCreateNativeLoginFlowRequest /* * CreateNativeLoginFlowExecute executes the request * @return LoginFlow */ - CreateNativeLoginFlowExecute(r FrontendApiApiCreateNativeLoginFlowRequest) (*LoginFlow, *http.Response, error) + CreateNativeLoginFlowExecute(r FrontendAPIApiCreateNativeLoginFlowRequest) (*LoginFlow, *http.Response, error) /* * CreateNativeRecoveryFlow Create Recovery Flow for Native Apps @@ -250,15 +250,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeRecoveryFlowRequest + * @return FrontendAPIApiCreateNativeRecoveryFlowRequest */ - CreateNativeRecoveryFlow(ctx context.Context) FrontendApiApiCreateNativeRecoveryFlowRequest + CreateNativeRecoveryFlow(ctx context.Context) FrontendAPIApiCreateNativeRecoveryFlowRequest /* * CreateNativeRecoveryFlowExecute executes the request * @return RecoveryFlow */ - CreateNativeRecoveryFlowExecute(r FrontendApiApiCreateNativeRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) + CreateNativeRecoveryFlowExecute(r FrontendAPIApiCreateNativeRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) /* * CreateNativeRegistrationFlow Create Registration Flow for Native Apps @@ -282,15 +282,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeRegistrationFlowRequest + * @return FrontendAPIApiCreateNativeRegistrationFlowRequest */ - CreateNativeRegistrationFlow(ctx context.Context) FrontendApiApiCreateNativeRegistrationFlowRequest + CreateNativeRegistrationFlow(ctx context.Context) FrontendAPIApiCreateNativeRegistrationFlowRequest /* * CreateNativeRegistrationFlowExecute executes the request * @return RegistrationFlow */ - CreateNativeRegistrationFlowExecute(r FrontendApiApiCreateNativeRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) + CreateNativeRegistrationFlowExecute(r FrontendAPIApiCreateNativeRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) /* * CreateNativeSettingsFlow Create Settings Flow for Native Apps @@ -317,15 +317,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeSettingsFlowRequest + * @return FrontendAPIApiCreateNativeSettingsFlowRequest */ - CreateNativeSettingsFlow(ctx context.Context) FrontendApiApiCreateNativeSettingsFlowRequest + CreateNativeSettingsFlow(ctx context.Context) FrontendAPIApiCreateNativeSettingsFlowRequest /* * CreateNativeSettingsFlowExecute executes the request * @return SettingsFlow */ - CreateNativeSettingsFlowExecute(r FrontendApiApiCreateNativeSettingsFlowRequest) (*SettingsFlow, *http.Response, error) + CreateNativeSettingsFlowExecute(r FrontendAPIApiCreateNativeSettingsFlowRequest) (*SettingsFlow, *http.Response, error) /* * CreateNativeVerificationFlow Create Verification Flow for Native Apps @@ -341,30 +341,30 @@ type FrontendApi interface { More information can be found at [Ory Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeVerificationFlowRequest + * @return FrontendAPIApiCreateNativeVerificationFlowRequest */ - CreateNativeVerificationFlow(ctx context.Context) FrontendApiApiCreateNativeVerificationFlowRequest + CreateNativeVerificationFlow(ctx context.Context) FrontendAPIApiCreateNativeVerificationFlowRequest /* * CreateNativeVerificationFlowExecute executes the request * @return VerificationFlow */ - CreateNativeVerificationFlowExecute(r FrontendApiApiCreateNativeVerificationFlowRequest) (*VerificationFlow, *http.Response, error) + CreateNativeVerificationFlowExecute(r FrontendAPIApiCreateNativeVerificationFlowRequest) (*VerificationFlow, *http.Response, error) /* * DisableMyOtherSessions Disable my other sessions * Calling this endpoint invalidates all except the current session that belong to the logged-in user. Session data are not deleted. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiDisableMyOtherSessionsRequest + * @return FrontendAPIApiDisableMyOtherSessionsRequest */ - DisableMyOtherSessions(ctx context.Context) FrontendApiApiDisableMyOtherSessionsRequest + DisableMyOtherSessions(ctx context.Context) FrontendAPIApiDisableMyOtherSessionsRequest /* * DisableMyOtherSessionsExecute executes the request * @return DeleteMySessionsCount */ - DisableMyOtherSessionsExecute(r FrontendApiApiDisableMyOtherSessionsRequest) (*DeleteMySessionsCount, *http.Response, error) + DisableMyOtherSessionsExecute(r FrontendAPIApiDisableMyOtherSessionsRequest) (*DeleteMySessionsCount, *http.Response, error) /* * DisableMySession Disable one of my sessions @@ -372,27 +372,27 @@ type FrontendApi interface { Session data are not deleted. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the session's ID. - * @return FrontendApiApiDisableMySessionRequest + * @return FrontendAPIApiDisableMySessionRequest */ - DisableMySession(ctx context.Context, id string) FrontendApiApiDisableMySessionRequest + DisableMySession(ctx context.Context, id string) FrontendAPIApiDisableMySessionRequest /* * DisableMySessionExecute executes the request */ - DisableMySessionExecute(r FrontendApiApiDisableMySessionRequest) (*http.Response, error) + DisableMySessionExecute(r FrontendAPIApiDisableMySessionRequest) (*http.Response, error) /* * ExchangeSessionToken Exchange Session Token * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiExchangeSessionTokenRequest + * @return FrontendAPIApiExchangeSessionTokenRequest */ - ExchangeSessionToken(ctx context.Context) FrontendApiApiExchangeSessionTokenRequest + ExchangeSessionToken(ctx context.Context) FrontendAPIApiExchangeSessionTokenRequest /* * ExchangeSessionTokenExecute executes the request * @return SuccessfulNativeLogin */ - ExchangeSessionTokenExecute(r FrontendApiApiExchangeSessionTokenRequest) (*SuccessfulNativeLogin, *http.Response, error) + ExchangeSessionTokenExecute(r FrontendAPIApiExchangeSessionTokenRequest) (*SuccessfulNativeLogin, *http.Response, error) /* * GetFlowError Get User-Flow Errors @@ -404,15 +404,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User User Facing Error Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-facing-errors). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetFlowErrorRequest + * @return FrontendAPIApiGetFlowErrorRequest */ - GetFlowError(ctx context.Context) FrontendApiApiGetFlowErrorRequest + GetFlowError(ctx context.Context) FrontendAPIApiGetFlowErrorRequest /* * GetFlowErrorExecute executes the request * @return FlowError */ - GetFlowErrorExecute(r FrontendApiApiGetFlowErrorRequest) (*FlowError, *http.Response, error) + GetFlowErrorExecute(r FrontendAPIApiGetFlowErrorRequest) (*FlowError, *http.Response, error) /* * GetLoginFlow Get Login Flow @@ -440,15 +440,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetLoginFlowRequest + * @return FrontendAPIApiGetLoginFlowRequest */ - GetLoginFlow(ctx context.Context) FrontendApiApiGetLoginFlowRequest + GetLoginFlow(ctx context.Context) FrontendAPIApiGetLoginFlowRequest /* * GetLoginFlowExecute executes the request * @return LoginFlow */ - GetLoginFlowExecute(r FrontendApiApiGetLoginFlowRequest) (*LoginFlow, *http.Response, error) + GetLoginFlowExecute(r FrontendAPIApiGetLoginFlowRequest) (*LoginFlow, *http.Response, error) /* * GetRecoveryFlow Get Recovery Flow @@ -471,15 +471,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetRecoveryFlowRequest + * @return FrontendAPIApiGetRecoveryFlowRequest */ - GetRecoveryFlow(ctx context.Context) FrontendApiApiGetRecoveryFlowRequest + GetRecoveryFlow(ctx context.Context) FrontendAPIApiGetRecoveryFlowRequest /* * GetRecoveryFlowExecute executes the request * @return RecoveryFlow */ - GetRecoveryFlowExecute(r FrontendApiApiGetRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) + GetRecoveryFlowExecute(r FrontendAPIApiGetRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) /* * GetRegistrationFlow Get Registration Flow @@ -507,15 +507,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetRegistrationFlowRequest + * @return FrontendAPIApiGetRegistrationFlowRequest */ - GetRegistrationFlow(ctx context.Context) FrontendApiApiGetRegistrationFlowRequest + GetRegistrationFlow(ctx context.Context) FrontendAPIApiGetRegistrationFlowRequest /* * GetRegistrationFlowExecute executes the request * @return RegistrationFlow */ - GetRegistrationFlowExecute(r FrontendApiApiGetRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) + GetRegistrationFlowExecute(r FrontendAPIApiGetRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) /* * GetSettingsFlow Get Settings Flow @@ -539,15 +539,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetSettingsFlowRequest + * @return FrontendAPIApiGetSettingsFlowRequest */ - GetSettingsFlow(ctx context.Context) FrontendApiApiGetSettingsFlowRequest + GetSettingsFlow(ctx context.Context) FrontendAPIApiGetSettingsFlowRequest /* * GetSettingsFlowExecute executes the request * @return SettingsFlow */ - GetSettingsFlowExecute(r FrontendApiApiGetSettingsFlowRequest) (*SettingsFlow, *http.Response, error) + GetSettingsFlowExecute(r FrontendAPIApiGetSettingsFlowRequest) (*SettingsFlow, *http.Response, error) /* * GetVerificationFlow Get Verification Flow @@ -570,15 +570,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetVerificationFlowRequest + * @return FrontendAPIApiGetVerificationFlowRequest */ - GetVerificationFlow(ctx context.Context) FrontendApiApiGetVerificationFlowRequest + GetVerificationFlow(ctx context.Context) FrontendAPIApiGetVerificationFlowRequest /* * GetVerificationFlowExecute executes the request * @return VerificationFlow */ - GetVerificationFlowExecute(r FrontendApiApiGetVerificationFlowRequest) (*VerificationFlow, *http.Response, error) + GetVerificationFlowExecute(r FrontendAPIApiGetVerificationFlowRequest) (*VerificationFlow, *http.Response, error) /* * GetWebAuthnJavaScript Get WebAuthn JavaScript @@ -592,30 +592,30 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetWebAuthnJavaScriptRequest + * @return FrontendAPIApiGetWebAuthnJavaScriptRequest */ - GetWebAuthnJavaScript(ctx context.Context) FrontendApiApiGetWebAuthnJavaScriptRequest + GetWebAuthnJavaScript(ctx context.Context) FrontendAPIApiGetWebAuthnJavaScriptRequest /* * GetWebAuthnJavaScriptExecute executes the request * @return string */ - GetWebAuthnJavaScriptExecute(r FrontendApiApiGetWebAuthnJavaScriptRequest) (string, *http.Response, error) + GetWebAuthnJavaScriptExecute(r FrontendAPIApiGetWebAuthnJavaScriptRequest) (string, *http.Response, error) /* * ListMySessions Get My Active Sessions * This endpoints returns all other active sessions that belong to the logged-in user. The current session can be retrieved by calling the `/sessions/whoami` endpoint. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiListMySessionsRequest + * @return FrontendAPIApiListMySessionsRequest */ - ListMySessions(ctx context.Context) FrontendApiApiListMySessionsRequest + ListMySessions(ctx context.Context) FrontendAPIApiListMySessionsRequest /* * ListMySessionsExecute executes the request * @return []Session */ - ListMySessionsExecute(r FrontendApiApiListMySessionsRequest) ([]Session, *http.Response, error) + ListMySessionsExecute(r FrontendAPIApiListMySessionsRequest) ([]Session, *http.Response, error) /* * PerformNativeLogout Perform Logout for Native Apps @@ -628,14 +628,14 @@ type FrontendApi interface { This endpoint does not remove any HTTP Cookies - use the Browser-Based Self-Service Logout Flow instead. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiPerformNativeLogoutRequest + * @return FrontendAPIApiPerformNativeLogoutRequest */ - PerformNativeLogout(ctx context.Context) FrontendApiApiPerformNativeLogoutRequest + PerformNativeLogout(ctx context.Context) FrontendAPIApiPerformNativeLogoutRequest /* * PerformNativeLogoutExecute executes the request */ - PerformNativeLogoutExecute(r FrontendApiApiPerformNativeLogoutRequest) (*http.Response, error) + PerformNativeLogoutExecute(r FrontendAPIApiPerformNativeLogoutRequest) (*http.Response, error) /* * ToSession Check Who the Current HTTP Session Belongs To @@ -700,15 +700,15 @@ type FrontendApi interface { `session_inactive`: No active session was found in the request (e.g. no Ory Session Cookie / Ory Session Token). `session_aal2_required`: An active session was found but it does not fulfil the Authenticator Assurance Level, implying that the session must (e.g.) authenticate the second factor. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiToSessionRequest + * @return FrontendAPIApiToSessionRequest */ - ToSession(ctx context.Context) FrontendApiApiToSessionRequest + ToSession(ctx context.Context) FrontendAPIApiToSessionRequest /* * ToSessionExecute executes the request * @return Session */ - ToSessionExecute(r FrontendApiApiToSessionRequest) (*Session, *http.Response, error) + ToSessionExecute(r FrontendAPIApiToSessionRequest) (*Session, *http.Response, error) /* * UpdateLoginFlow Submit a Login Flow @@ -740,15 +740,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiUpdateLoginFlowRequest + * @return FrontendAPIApiUpdateLoginFlowRequest */ - UpdateLoginFlow(ctx context.Context) FrontendApiApiUpdateLoginFlowRequest + UpdateLoginFlow(ctx context.Context) FrontendAPIApiUpdateLoginFlowRequest /* * UpdateLoginFlowExecute executes the request * @return SuccessfulNativeLogin */ - UpdateLoginFlowExecute(r FrontendApiApiUpdateLoginFlowRequest) (*SuccessfulNativeLogin, *http.Response, error) + UpdateLoginFlowExecute(r FrontendAPIApiUpdateLoginFlowRequest) (*SuccessfulNativeLogin, *http.Response, error) /* * UpdateLogoutFlow Update Logout Flow @@ -766,14 +766,14 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Logout Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-logout). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiUpdateLogoutFlowRequest + * @return FrontendAPIApiUpdateLogoutFlowRequest */ - UpdateLogoutFlow(ctx context.Context) FrontendApiApiUpdateLogoutFlowRequest + UpdateLogoutFlow(ctx context.Context) FrontendAPIApiUpdateLogoutFlowRequest /* * UpdateLogoutFlowExecute executes the request */ - UpdateLogoutFlowExecute(r FrontendApiApiUpdateLogoutFlowRequest) (*http.Response, error) + UpdateLogoutFlowExecute(r FrontendAPIApiUpdateLogoutFlowRequest) (*http.Response, error) /* * UpdateRecoveryFlow Update Recovery Flow @@ -794,15 +794,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiUpdateRecoveryFlowRequest + * @return FrontendAPIApiUpdateRecoveryFlowRequest */ - UpdateRecoveryFlow(ctx context.Context) FrontendApiApiUpdateRecoveryFlowRequest + UpdateRecoveryFlow(ctx context.Context) FrontendAPIApiUpdateRecoveryFlowRequest /* * UpdateRecoveryFlowExecute executes the request * @return RecoveryFlow */ - UpdateRecoveryFlowExecute(r FrontendApiApiUpdateRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) + UpdateRecoveryFlowExecute(r FrontendAPIApiUpdateRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) /* * UpdateRegistrationFlow Update Registration Flow @@ -835,15 +835,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiUpdateRegistrationFlowRequest + * @return FrontendAPIApiUpdateRegistrationFlowRequest */ - UpdateRegistrationFlow(ctx context.Context) FrontendApiApiUpdateRegistrationFlowRequest + UpdateRegistrationFlow(ctx context.Context) FrontendAPIApiUpdateRegistrationFlowRequest /* * UpdateRegistrationFlowExecute executes the request * @return SuccessfulNativeRegistration */ - UpdateRegistrationFlowExecute(r FrontendApiApiUpdateRegistrationFlowRequest) (*SuccessfulNativeRegistration, *http.Response, error) + UpdateRegistrationFlowExecute(r FrontendAPIApiUpdateRegistrationFlowRequest) (*SuccessfulNativeRegistration, *http.Response, error) /* * UpdateSettingsFlow Complete Settings Flow @@ -891,15 +891,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiUpdateSettingsFlowRequest + * @return FrontendAPIApiUpdateSettingsFlowRequest */ - UpdateSettingsFlow(ctx context.Context) FrontendApiApiUpdateSettingsFlowRequest + UpdateSettingsFlow(ctx context.Context) FrontendAPIApiUpdateSettingsFlowRequest /* * UpdateSettingsFlowExecute executes the request * @return SettingsFlow */ - UpdateSettingsFlowExecute(r FrontendApiApiUpdateSettingsFlowRequest) (*SettingsFlow, *http.Response, error) + UpdateSettingsFlowExecute(r FrontendAPIApiUpdateSettingsFlowRequest) (*SettingsFlow, *http.Response, error) /* * UpdateVerificationFlow Complete Verification Flow @@ -920,57 +920,62 @@ type FrontendApi interface { More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiUpdateVerificationFlowRequest + * @return FrontendAPIApiUpdateVerificationFlowRequest */ - UpdateVerificationFlow(ctx context.Context) FrontendApiApiUpdateVerificationFlowRequest + UpdateVerificationFlow(ctx context.Context) FrontendAPIApiUpdateVerificationFlowRequest /* * UpdateVerificationFlowExecute executes the request * @return VerificationFlow */ - UpdateVerificationFlowExecute(r FrontendApiApiUpdateVerificationFlowRequest) (*VerificationFlow, *http.Response, error) + UpdateVerificationFlowExecute(r FrontendAPIApiUpdateVerificationFlowRequest) (*VerificationFlow, *http.Response, error) } -// FrontendApiService FrontendApi service -type FrontendApiService service +// FrontendAPIService FrontendAPI service +type FrontendAPIService service -type FrontendApiApiCreateBrowserLoginFlowRequest struct { +type FrontendAPIApiCreateBrowserLoginFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI refresh *bool aal *string returnTo *string cookie *string loginChallenge *string organization *string + via *string } -func (r FrontendApiApiCreateBrowserLoginFlowRequest) Refresh(refresh bool) FrontendApiApiCreateBrowserLoginFlowRequest { +func (r FrontendAPIApiCreateBrowserLoginFlowRequest) Refresh(refresh bool) FrontendAPIApiCreateBrowserLoginFlowRequest { r.refresh = &refresh return r } -func (r FrontendApiApiCreateBrowserLoginFlowRequest) Aal(aal string) FrontendApiApiCreateBrowserLoginFlowRequest { +func (r FrontendAPIApiCreateBrowserLoginFlowRequest) Aal(aal string) FrontendAPIApiCreateBrowserLoginFlowRequest { r.aal = &aal return r } -func (r FrontendApiApiCreateBrowserLoginFlowRequest) ReturnTo(returnTo string) FrontendApiApiCreateBrowserLoginFlowRequest { +func (r FrontendAPIApiCreateBrowserLoginFlowRequest) ReturnTo(returnTo string) FrontendAPIApiCreateBrowserLoginFlowRequest { r.returnTo = &returnTo return r } -func (r FrontendApiApiCreateBrowserLoginFlowRequest) Cookie(cookie string) FrontendApiApiCreateBrowserLoginFlowRequest { +func (r FrontendAPIApiCreateBrowserLoginFlowRequest) Cookie(cookie string) FrontendAPIApiCreateBrowserLoginFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiCreateBrowserLoginFlowRequest) LoginChallenge(loginChallenge string) FrontendApiApiCreateBrowserLoginFlowRequest { +func (r FrontendAPIApiCreateBrowserLoginFlowRequest) LoginChallenge(loginChallenge string) FrontendAPIApiCreateBrowserLoginFlowRequest { r.loginChallenge = &loginChallenge return r } -func (r FrontendApiApiCreateBrowserLoginFlowRequest) Organization(organization string) FrontendApiApiCreateBrowserLoginFlowRequest { +func (r FrontendAPIApiCreateBrowserLoginFlowRequest) Organization(organization string) FrontendAPIApiCreateBrowserLoginFlowRequest { r.organization = &organization return r } +func (r FrontendAPIApiCreateBrowserLoginFlowRequest) Via(via string) FrontendAPIApiCreateBrowserLoginFlowRequest { + r.via = &via + return r +} -func (r FrontendApiApiCreateBrowserLoginFlowRequest) Execute() (*LoginFlow, *http.Response, error) { +func (r FrontendAPIApiCreateBrowserLoginFlowRequest) Execute() (*LoginFlow, *http.Response, error) { return r.ApiService.CreateBrowserLoginFlowExecute(r) } @@ -1001,10 +1006,10 @@ This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Fi More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateBrowserLoginFlowRequest + - @return FrontendAPIApiCreateBrowserLoginFlowRequest */ -func (a *FrontendApiService) CreateBrowserLoginFlow(ctx context.Context) FrontendApiApiCreateBrowserLoginFlowRequest { - return FrontendApiApiCreateBrowserLoginFlowRequest{ +func (a *FrontendAPIService) CreateBrowserLoginFlow(ctx context.Context) FrontendAPIApiCreateBrowserLoginFlowRequest { + return FrontendAPIApiCreateBrowserLoginFlowRequest{ ApiService: a, ctx: ctx, } @@ -1014,7 +1019,7 @@ func (a *FrontendApiService) CreateBrowserLoginFlow(ctx context.Context) Fronten * Execute executes the request * @return LoginFlow */ -func (a *FrontendApiService) CreateBrowserLoginFlowExecute(r FrontendApiApiCreateBrowserLoginFlowRequest) (*LoginFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateBrowserLoginFlowExecute(r FrontendAPIApiCreateBrowserLoginFlowRequest) (*LoginFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -1024,7 +1029,7 @@ func (a *FrontendApiService) CreateBrowserLoginFlowExecute(r FrontendApiApiCreat localVarReturnValue *LoginFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateBrowserLoginFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateBrowserLoginFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1050,6 +1055,9 @@ func (a *FrontendApiService) CreateBrowserLoginFlowExecute(r FrontendApiApiCreat if r.organization != nil { localVarQueryParams.Add("organization", parameterToString(*r.organization, "")) } + if r.via != nil { + localVarQueryParams.Add("via", parameterToString(*r.via, "")) + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} @@ -1080,7 +1088,7 @@ func (a *FrontendApiService) CreateBrowserLoginFlowExecute(r FrontendApiApiCreat return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1124,23 +1132,23 @@ func (a *FrontendApiService) CreateBrowserLoginFlowExecute(r FrontendApiApiCreat return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateBrowserLogoutFlowRequest struct { +type FrontendAPIApiCreateBrowserLogoutFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI cookie *string returnTo *string } -func (r FrontendApiApiCreateBrowserLogoutFlowRequest) Cookie(cookie string) FrontendApiApiCreateBrowserLogoutFlowRequest { +func (r FrontendAPIApiCreateBrowserLogoutFlowRequest) Cookie(cookie string) FrontendAPIApiCreateBrowserLogoutFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiCreateBrowserLogoutFlowRequest) ReturnTo(returnTo string) FrontendApiApiCreateBrowserLogoutFlowRequest { +func (r FrontendAPIApiCreateBrowserLogoutFlowRequest) ReturnTo(returnTo string) FrontendAPIApiCreateBrowserLogoutFlowRequest { r.returnTo = &returnTo return r } -func (r FrontendApiApiCreateBrowserLogoutFlowRequest) Execute() (*LogoutFlow, *http.Response, error) { +func (r FrontendAPIApiCreateBrowserLogoutFlowRequest) Execute() (*LogoutFlow, *http.Response, error) { return r.ApiService.CreateBrowserLogoutFlowExecute(r) } @@ -1157,10 +1165,10 @@ a 401 error. When calling this endpoint from a backend, please ensure to properly forward the HTTP cookies. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateBrowserLogoutFlowRequest + - @return FrontendAPIApiCreateBrowserLogoutFlowRequest */ -func (a *FrontendApiService) CreateBrowserLogoutFlow(ctx context.Context) FrontendApiApiCreateBrowserLogoutFlowRequest { - return FrontendApiApiCreateBrowserLogoutFlowRequest{ +func (a *FrontendAPIService) CreateBrowserLogoutFlow(ctx context.Context) FrontendAPIApiCreateBrowserLogoutFlowRequest { + return FrontendAPIApiCreateBrowserLogoutFlowRequest{ ApiService: a, ctx: ctx, } @@ -1170,7 +1178,7 @@ func (a *FrontendApiService) CreateBrowserLogoutFlow(ctx context.Context) Fronte * Execute executes the request * @return LogoutFlow */ -func (a *FrontendApiService) CreateBrowserLogoutFlowExecute(r FrontendApiApiCreateBrowserLogoutFlowRequest) (*LogoutFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateBrowserLogoutFlowExecute(r FrontendAPIApiCreateBrowserLogoutFlowRequest) (*LogoutFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -1180,7 +1188,7 @@ func (a *FrontendApiService) CreateBrowserLogoutFlowExecute(r FrontendApiApiCrea localVarReturnValue *LogoutFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateBrowserLogoutFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateBrowserLogoutFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1224,7 +1232,7 @@ func (a *FrontendApiService) CreateBrowserLogoutFlowExecute(r FrontendApiApiCrea return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1280,18 +1288,18 @@ func (a *FrontendApiService) CreateBrowserLogoutFlowExecute(r FrontendApiApiCrea return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateBrowserRecoveryFlowRequest struct { +type FrontendAPIApiCreateBrowserRecoveryFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI returnTo *string } -func (r FrontendApiApiCreateBrowserRecoveryFlowRequest) ReturnTo(returnTo string) FrontendApiApiCreateBrowserRecoveryFlowRequest { +func (r FrontendAPIApiCreateBrowserRecoveryFlowRequest) ReturnTo(returnTo string) FrontendAPIApiCreateBrowserRecoveryFlowRequest { r.returnTo = &returnTo return r } -func (r FrontendApiApiCreateBrowserRecoveryFlowRequest) Execute() (*RecoveryFlow, *http.Response, error) { +func (r FrontendAPIApiCreateBrowserRecoveryFlowRequest) Execute() (*RecoveryFlow, *http.Response, error) { return r.ApiService.CreateBrowserRecoveryFlowExecute(r) } @@ -1309,10 +1317,10 @@ This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Fi More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateBrowserRecoveryFlowRequest + - @return FrontendAPIApiCreateBrowserRecoveryFlowRequest */ -func (a *FrontendApiService) CreateBrowserRecoveryFlow(ctx context.Context) FrontendApiApiCreateBrowserRecoveryFlowRequest { - return FrontendApiApiCreateBrowserRecoveryFlowRequest{ +func (a *FrontendAPIService) CreateBrowserRecoveryFlow(ctx context.Context) FrontendAPIApiCreateBrowserRecoveryFlowRequest { + return FrontendAPIApiCreateBrowserRecoveryFlowRequest{ ApiService: a, ctx: ctx, } @@ -1322,7 +1330,7 @@ func (a *FrontendApiService) CreateBrowserRecoveryFlow(ctx context.Context) Fron * Execute executes the request * @return RecoveryFlow */ -func (a *FrontendApiService) CreateBrowserRecoveryFlowExecute(r FrontendApiApiCreateBrowserRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateBrowserRecoveryFlowExecute(r FrontendAPIApiCreateBrowserRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -1332,7 +1340,7 @@ func (a *FrontendApiService) CreateBrowserRecoveryFlowExecute(r FrontendApiApiCr localVarReturnValue *RecoveryFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateBrowserRecoveryFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateBrowserRecoveryFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1373,7 +1381,7 @@ func (a *FrontendApiService) CreateBrowserRecoveryFlowExecute(r FrontendApiApiCr return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1417,33 +1425,33 @@ func (a *FrontendApiService) CreateBrowserRecoveryFlowExecute(r FrontendApiApiCr return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateBrowserRegistrationFlowRequest struct { +type FrontendAPIApiCreateBrowserRegistrationFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI returnTo *string loginChallenge *string afterVerificationReturnTo *string organization *string } -func (r FrontendApiApiCreateBrowserRegistrationFlowRequest) ReturnTo(returnTo string) FrontendApiApiCreateBrowserRegistrationFlowRequest { +func (r FrontendAPIApiCreateBrowserRegistrationFlowRequest) ReturnTo(returnTo string) FrontendAPIApiCreateBrowserRegistrationFlowRequest { r.returnTo = &returnTo return r } -func (r FrontendApiApiCreateBrowserRegistrationFlowRequest) LoginChallenge(loginChallenge string) FrontendApiApiCreateBrowserRegistrationFlowRequest { +func (r FrontendAPIApiCreateBrowserRegistrationFlowRequest) LoginChallenge(loginChallenge string) FrontendAPIApiCreateBrowserRegistrationFlowRequest { r.loginChallenge = &loginChallenge return r } -func (r FrontendApiApiCreateBrowserRegistrationFlowRequest) AfterVerificationReturnTo(afterVerificationReturnTo string) FrontendApiApiCreateBrowserRegistrationFlowRequest { +func (r FrontendAPIApiCreateBrowserRegistrationFlowRequest) AfterVerificationReturnTo(afterVerificationReturnTo string) FrontendAPIApiCreateBrowserRegistrationFlowRequest { r.afterVerificationReturnTo = &afterVerificationReturnTo return r } -func (r FrontendApiApiCreateBrowserRegistrationFlowRequest) Organization(organization string) FrontendApiApiCreateBrowserRegistrationFlowRequest { +func (r FrontendAPIApiCreateBrowserRegistrationFlowRequest) Organization(organization string) FrontendAPIApiCreateBrowserRegistrationFlowRequest { r.organization = &organization return r } -func (r FrontendApiApiCreateBrowserRegistrationFlowRequest) Execute() (*RegistrationFlow, *http.Response, error) { +func (r FrontendAPIApiCreateBrowserRegistrationFlowRequest) Execute() (*RegistrationFlow, *http.Response, error) { return r.ApiService.CreateBrowserRegistrationFlowExecute(r) } @@ -1470,10 +1478,10 @@ This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Fi More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateBrowserRegistrationFlowRequest + - @return FrontendAPIApiCreateBrowserRegistrationFlowRequest */ -func (a *FrontendApiService) CreateBrowserRegistrationFlow(ctx context.Context) FrontendApiApiCreateBrowserRegistrationFlowRequest { - return FrontendApiApiCreateBrowserRegistrationFlowRequest{ +func (a *FrontendAPIService) CreateBrowserRegistrationFlow(ctx context.Context) FrontendAPIApiCreateBrowserRegistrationFlowRequest { + return FrontendAPIApiCreateBrowserRegistrationFlowRequest{ ApiService: a, ctx: ctx, } @@ -1483,7 +1491,7 @@ func (a *FrontendApiService) CreateBrowserRegistrationFlow(ctx context.Context) * Execute executes the request * @return RegistrationFlow */ -func (a *FrontendApiService) CreateBrowserRegistrationFlowExecute(r FrontendApiApiCreateBrowserRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateBrowserRegistrationFlowExecute(r FrontendAPIApiCreateBrowserRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -1493,7 +1501,7 @@ func (a *FrontendApiService) CreateBrowserRegistrationFlowExecute(r FrontendApiA localVarReturnValue *RegistrationFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateBrowserRegistrationFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateBrowserRegistrationFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1543,7 +1551,7 @@ func (a *FrontendApiService) CreateBrowserRegistrationFlowExecute(r FrontendApiA return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1577,23 +1585,23 @@ func (a *FrontendApiService) CreateBrowserRegistrationFlowExecute(r FrontendApiA return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateBrowserSettingsFlowRequest struct { +type FrontendAPIApiCreateBrowserSettingsFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI returnTo *string cookie *string } -func (r FrontendApiApiCreateBrowserSettingsFlowRequest) ReturnTo(returnTo string) FrontendApiApiCreateBrowserSettingsFlowRequest { +func (r FrontendAPIApiCreateBrowserSettingsFlowRequest) ReturnTo(returnTo string) FrontendAPIApiCreateBrowserSettingsFlowRequest { r.returnTo = &returnTo return r } -func (r FrontendApiApiCreateBrowserSettingsFlowRequest) Cookie(cookie string) FrontendApiApiCreateBrowserSettingsFlowRequest { +func (r FrontendAPIApiCreateBrowserSettingsFlowRequest) Cookie(cookie string) FrontendAPIApiCreateBrowserSettingsFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiCreateBrowserSettingsFlowRequest) Execute() (*SettingsFlow, *http.Response, error) { +func (r FrontendAPIApiCreateBrowserSettingsFlowRequest) Execute() (*SettingsFlow, *http.Response, error) { return r.ApiService.CreateBrowserSettingsFlowExecute(r) } @@ -1627,10 +1635,10 @@ This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Fi More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateBrowserSettingsFlowRequest + - @return FrontendAPIApiCreateBrowserSettingsFlowRequest */ -func (a *FrontendApiService) CreateBrowserSettingsFlow(ctx context.Context) FrontendApiApiCreateBrowserSettingsFlowRequest { - return FrontendApiApiCreateBrowserSettingsFlowRequest{ +func (a *FrontendAPIService) CreateBrowserSettingsFlow(ctx context.Context) FrontendAPIApiCreateBrowserSettingsFlowRequest { + return FrontendAPIApiCreateBrowserSettingsFlowRequest{ ApiService: a, ctx: ctx, } @@ -1640,7 +1648,7 @@ func (a *FrontendApiService) CreateBrowserSettingsFlow(ctx context.Context) Fron * Execute executes the request * @return SettingsFlow */ -func (a *FrontendApiService) CreateBrowserSettingsFlowExecute(r FrontendApiApiCreateBrowserSettingsFlowRequest) (*SettingsFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateBrowserSettingsFlowExecute(r FrontendAPIApiCreateBrowserSettingsFlowRequest) (*SettingsFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -1650,7 +1658,7 @@ func (a *FrontendApiService) CreateBrowserSettingsFlowExecute(r FrontendApiApiCr localVarReturnValue *SettingsFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateBrowserSettingsFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateBrowserSettingsFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1694,7 +1702,7 @@ func (a *FrontendApiService) CreateBrowserSettingsFlowExecute(r FrontendApiApiCr return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1758,18 +1766,18 @@ func (a *FrontendApiService) CreateBrowserSettingsFlowExecute(r FrontendApiApiCr return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateBrowserVerificationFlowRequest struct { +type FrontendAPIApiCreateBrowserVerificationFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI returnTo *string } -func (r FrontendApiApiCreateBrowserVerificationFlowRequest) ReturnTo(returnTo string) FrontendApiApiCreateBrowserVerificationFlowRequest { +func (r FrontendAPIApiCreateBrowserVerificationFlowRequest) ReturnTo(returnTo string) FrontendAPIApiCreateBrowserVerificationFlowRequest { r.returnTo = &returnTo return r } -func (r FrontendApiApiCreateBrowserVerificationFlowRequest) Execute() (*VerificationFlow, *http.Response, error) { +func (r FrontendAPIApiCreateBrowserVerificationFlowRequest) Execute() (*VerificationFlow, *http.Response, error) { return r.ApiService.CreateBrowserVerificationFlowExecute(r) } @@ -1785,10 +1793,10 @@ This endpoint is NOT INTENDED for API clients and only works with browsers (Chro More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateBrowserVerificationFlowRequest + - @return FrontendAPIApiCreateBrowserVerificationFlowRequest */ -func (a *FrontendApiService) CreateBrowserVerificationFlow(ctx context.Context) FrontendApiApiCreateBrowserVerificationFlowRequest { - return FrontendApiApiCreateBrowserVerificationFlowRequest{ +func (a *FrontendAPIService) CreateBrowserVerificationFlow(ctx context.Context) FrontendAPIApiCreateBrowserVerificationFlowRequest { + return FrontendAPIApiCreateBrowserVerificationFlowRequest{ ApiService: a, ctx: ctx, } @@ -1798,7 +1806,7 @@ func (a *FrontendApiService) CreateBrowserVerificationFlow(ctx context.Context) * Execute executes the request * @return VerificationFlow */ -func (a *FrontendApiService) CreateBrowserVerificationFlowExecute(r FrontendApiApiCreateBrowserVerificationFlowRequest) (*VerificationFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateBrowserVerificationFlowExecute(r FrontendAPIApiCreateBrowserVerificationFlowRequest) (*VerificationFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -1808,7 +1816,7 @@ func (a *FrontendApiService) CreateBrowserVerificationFlowExecute(r FrontendApiA localVarReturnValue *VerificationFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateBrowserVerificationFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateBrowserVerificationFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1849,7 +1857,7 @@ func (a *FrontendApiService) CreateBrowserVerificationFlowExecute(r FrontendApiA return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1883,9 +1891,9 @@ func (a *FrontendApiService) CreateBrowserVerificationFlowExecute(r FrontendApiA return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateNativeLoginFlowRequest struct { +type FrontendAPIApiCreateNativeLoginFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI refresh *bool aal *string xSessionToken *string @@ -1894,32 +1902,32 @@ type FrontendApiApiCreateNativeLoginFlowRequest struct { via *string } -func (r FrontendApiApiCreateNativeLoginFlowRequest) Refresh(refresh bool) FrontendApiApiCreateNativeLoginFlowRequest { +func (r FrontendAPIApiCreateNativeLoginFlowRequest) Refresh(refresh bool) FrontendAPIApiCreateNativeLoginFlowRequest { r.refresh = &refresh return r } -func (r FrontendApiApiCreateNativeLoginFlowRequest) Aal(aal string) FrontendApiApiCreateNativeLoginFlowRequest { +func (r FrontendAPIApiCreateNativeLoginFlowRequest) Aal(aal string) FrontendAPIApiCreateNativeLoginFlowRequest { r.aal = &aal return r } -func (r FrontendApiApiCreateNativeLoginFlowRequest) XSessionToken(xSessionToken string) FrontendApiApiCreateNativeLoginFlowRequest { +func (r FrontendAPIApiCreateNativeLoginFlowRequest) XSessionToken(xSessionToken string) FrontendAPIApiCreateNativeLoginFlowRequest { r.xSessionToken = &xSessionToken return r } -func (r FrontendApiApiCreateNativeLoginFlowRequest) ReturnSessionTokenExchangeCode(returnSessionTokenExchangeCode bool) FrontendApiApiCreateNativeLoginFlowRequest { +func (r FrontendAPIApiCreateNativeLoginFlowRequest) ReturnSessionTokenExchangeCode(returnSessionTokenExchangeCode bool) FrontendAPIApiCreateNativeLoginFlowRequest { r.returnSessionTokenExchangeCode = &returnSessionTokenExchangeCode return r } -func (r FrontendApiApiCreateNativeLoginFlowRequest) ReturnTo(returnTo string) FrontendApiApiCreateNativeLoginFlowRequest { +func (r FrontendAPIApiCreateNativeLoginFlowRequest) ReturnTo(returnTo string) FrontendAPIApiCreateNativeLoginFlowRequest { r.returnTo = &returnTo return r } -func (r FrontendApiApiCreateNativeLoginFlowRequest) Via(via string) FrontendApiApiCreateNativeLoginFlowRequest { +func (r FrontendAPIApiCreateNativeLoginFlowRequest) Via(via string) FrontendAPIApiCreateNativeLoginFlowRequest { r.via = &via return r } -func (r FrontendApiApiCreateNativeLoginFlowRequest) Execute() (*LoginFlow, *http.Response, error) { +func (r FrontendAPIApiCreateNativeLoginFlowRequest) Execute() (*LoginFlow, *http.Response, error) { return r.ApiService.CreateNativeLoginFlowExecute(r) } @@ -1946,10 +1954,10 @@ This endpoint MUST ONLY be used in scenarios such as native mobile apps (React N More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateNativeLoginFlowRequest + - @return FrontendAPIApiCreateNativeLoginFlowRequest */ -func (a *FrontendApiService) CreateNativeLoginFlow(ctx context.Context) FrontendApiApiCreateNativeLoginFlowRequest { - return FrontendApiApiCreateNativeLoginFlowRequest{ +func (a *FrontendAPIService) CreateNativeLoginFlow(ctx context.Context) FrontendAPIApiCreateNativeLoginFlowRequest { + return FrontendAPIApiCreateNativeLoginFlowRequest{ ApiService: a, ctx: ctx, } @@ -1959,7 +1967,7 @@ func (a *FrontendApiService) CreateNativeLoginFlow(ctx context.Context) Frontend * Execute executes the request * @return LoginFlow */ -func (a *FrontendApiService) CreateNativeLoginFlowExecute(r FrontendApiApiCreateNativeLoginFlowRequest) (*LoginFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateNativeLoginFlowExecute(r FrontendAPIApiCreateNativeLoginFlowRequest) (*LoginFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -1969,7 +1977,7 @@ func (a *FrontendApiService) CreateNativeLoginFlowExecute(r FrontendApiApiCreate localVarReturnValue *LoginFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateNativeLoginFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateNativeLoginFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2025,7 +2033,7 @@ func (a *FrontendApiService) CreateNativeLoginFlowExecute(r FrontendApiApiCreate return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2069,12 +2077,12 @@ func (a *FrontendApiService) CreateNativeLoginFlowExecute(r FrontendApiApiCreate return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateNativeRecoveryFlowRequest struct { +type FrontendAPIApiCreateNativeRecoveryFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI } -func (r FrontendApiApiCreateNativeRecoveryFlowRequest) Execute() (*RecoveryFlow, *http.Response, error) { +func (r FrontendAPIApiCreateNativeRecoveryFlowRequest) Execute() (*RecoveryFlow, *http.Response, error) { return r.ApiService.CreateNativeRecoveryFlowExecute(r) } @@ -2094,10 +2102,10 @@ This endpoint MUST ONLY be used in scenarios such as native mobile apps (React N More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateNativeRecoveryFlowRequest + - @return FrontendAPIApiCreateNativeRecoveryFlowRequest */ -func (a *FrontendApiService) CreateNativeRecoveryFlow(ctx context.Context) FrontendApiApiCreateNativeRecoveryFlowRequest { - return FrontendApiApiCreateNativeRecoveryFlowRequest{ +func (a *FrontendAPIService) CreateNativeRecoveryFlow(ctx context.Context) FrontendAPIApiCreateNativeRecoveryFlowRequest { + return FrontendAPIApiCreateNativeRecoveryFlowRequest{ ApiService: a, ctx: ctx, } @@ -2107,7 +2115,7 @@ func (a *FrontendApiService) CreateNativeRecoveryFlow(ctx context.Context) Front * Execute executes the request * @return RecoveryFlow */ -func (a *FrontendApiService) CreateNativeRecoveryFlowExecute(r FrontendApiApiCreateNativeRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateNativeRecoveryFlowExecute(r FrontendAPIApiCreateNativeRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2117,7 +2125,7 @@ func (a *FrontendApiService) CreateNativeRecoveryFlowExecute(r FrontendApiApiCre localVarReturnValue *RecoveryFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateNativeRecoveryFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateNativeRecoveryFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2155,7 +2163,7 @@ func (a *FrontendApiService) CreateNativeRecoveryFlowExecute(r FrontendApiApiCre return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2199,23 +2207,23 @@ func (a *FrontendApiService) CreateNativeRecoveryFlowExecute(r FrontendApiApiCre return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateNativeRegistrationFlowRequest struct { +type FrontendAPIApiCreateNativeRegistrationFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI returnSessionTokenExchangeCode *bool returnTo *string } -func (r FrontendApiApiCreateNativeRegistrationFlowRequest) ReturnSessionTokenExchangeCode(returnSessionTokenExchangeCode bool) FrontendApiApiCreateNativeRegistrationFlowRequest { +func (r FrontendAPIApiCreateNativeRegistrationFlowRequest) ReturnSessionTokenExchangeCode(returnSessionTokenExchangeCode bool) FrontendAPIApiCreateNativeRegistrationFlowRequest { r.returnSessionTokenExchangeCode = &returnSessionTokenExchangeCode return r } -func (r FrontendApiApiCreateNativeRegistrationFlowRequest) ReturnTo(returnTo string) FrontendApiApiCreateNativeRegistrationFlowRequest { +func (r FrontendAPIApiCreateNativeRegistrationFlowRequest) ReturnTo(returnTo string) FrontendAPIApiCreateNativeRegistrationFlowRequest { r.returnTo = &returnTo return r } -func (r FrontendApiApiCreateNativeRegistrationFlowRequest) Execute() (*RegistrationFlow, *http.Response, error) { +func (r FrontendAPIApiCreateNativeRegistrationFlowRequest) Execute() (*RegistrationFlow, *http.Response, error) { return r.ApiService.CreateNativeRegistrationFlowExecute(r) } @@ -2241,10 +2249,10 @@ This endpoint MUST ONLY be used in scenarios such as native mobile apps (React N More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateNativeRegistrationFlowRequest + - @return FrontendAPIApiCreateNativeRegistrationFlowRequest */ -func (a *FrontendApiService) CreateNativeRegistrationFlow(ctx context.Context) FrontendApiApiCreateNativeRegistrationFlowRequest { - return FrontendApiApiCreateNativeRegistrationFlowRequest{ +func (a *FrontendAPIService) CreateNativeRegistrationFlow(ctx context.Context) FrontendAPIApiCreateNativeRegistrationFlowRequest { + return FrontendAPIApiCreateNativeRegistrationFlowRequest{ ApiService: a, ctx: ctx, } @@ -2254,7 +2262,7 @@ func (a *FrontendApiService) CreateNativeRegistrationFlow(ctx context.Context) F * Execute executes the request * @return RegistrationFlow */ -func (a *FrontendApiService) CreateNativeRegistrationFlowExecute(r FrontendApiApiCreateNativeRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateNativeRegistrationFlowExecute(r FrontendAPIApiCreateNativeRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2264,7 +2272,7 @@ func (a *FrontendApiService) CreateNativeRegistrationFlowExecute(r FrontendApiAp localVarReturnValue *RegistrationFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateNativeRegistrationFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateNativeRegistrationFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2308,7 +2316,7 @@ func (a *FrontendApiService) CreateNativeRegistrationFlowExecute(r FrontendApiAp return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2352,18 +2360,18 @@ func (a *FrontendApiService) CreateNativeRegistrationFlowExecute(r FrontendApiAp return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateNativeSettingsFlowRequest struct { +type FrontendAPIApiCreateNativeSettingsFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI xSessionToken *string } -func (r FrontendApiApiCreateNativeSettingsFlowRequest) XSessionToken(xSessionToken string) FrontendApiApiCreateNativeSettingsFlowRequest { +func (r FrontendAPIApiCreateNativeSettingsFlowRequest) XSessionToken(xSessionToken string) FrontendAPIApiCreateNativeSettingsFlowRequest { r.xSessionToken = &xSessionToken return r } -func (r FrontendApiApiCreateNativeSettingsFlowRequest) Execute() (*SettingsFlow, *http.Response, error) { +func (r FrontendAPIApiCreateNativeSettingsFlowRequest) Execute() (*SettingsFlow, *http.Response, error) { return r.ApiService.CreateNativeSettingsFlowExecute(r) } @@ -2393,10 +2401,10 @@ This endpoint MUST ONLY be used in scenarios such as native mobile apps (React N More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateNativeSettingsFlowRequest + - @return FrontendAPIApiCreateNativeSettingsFlowRequest */ -func (a *FrontendApiService) CreateNativeSettingsFlow(ctx context.Context) FrontendApiApiCreateNativeSettingsFlowRequest { - return FrontendApiApiCreateNativeSettingsFlowRequest{ +func (a *FrontendAPIService) CreateNativeSettingsFlow(ctx context.Context) FrontendAPIApiCreateNativeSettingsFlowRequest { + return FrontendAPIApiCreateNativeSettingsFlowRequest{ ApiService: a, ctx: ctx, } @@ -2406,7 +2414,7 @@ func (a *FrontendApiService) CreateNativeSettingsFlow(ctx context.Context) Front * Execute executes the request * @return SettingsFlow */ -func (a *FrontendApiService) CreateNativeSettingsFlowExecute(r FrontendApiApiCreateNativeSettingsFlowRequest) (*SettingsFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateNativeSettingsFlowExecute(r FrontendAPIApiCreateNativeSettingsFlowRequest) (*SettingsFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2416,7 +2424,7 @@ func (a *FrontendApiService) CreateNativeSettingsFlowExecute(r FrontendApiApiCre localVarReturnValue *SettingsFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateNativeSettingsFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateNativeSettingsFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2457,7 +2465,7 @@ func (a *FrontendApiService) CreateNativeSettingsFlowExecute(r FrontendApiApiCre return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2501,12 +2509,12 @@ func (a *FrontendApiService) CreateNativeSettingsFlowExecute(r FrontendApiApiCre return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateNativeVerificationFlowRequest struct { +type FrontendAPIApiCreateNativeVerificationFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI } -func (r FrontendApiApiCreateNativeVerificationFlowRequest) Execute() (*VerificationFlow, *http.Response, error) { +func (r FrontendAPIApiCreateNativeVerificationFlowRequest) Execute() (*VerificationFlow, *http.Response, error) { return r.ApiService.CreateNativeVerificationFlowExecute(r) } @@ -2524,10 +2532,10 @@ This endpoint MUST ONLY be used in scenarios such as native mobile apps (React N More information can be found at [Ory Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateNativeVerificationFlowRequest + - @return FrontendAPIApiCreateNativeVerificationFlowRequest */ -func (a *FrontendApiService) CreateNativeVerificationFlow(ctx context.Context) FrontendApiApiCreateNativeVerificationFlowRequest { - return FrontendApiApiCreateNativeVerificationFlowRequest{ +func (a *FrontendAPIService) CreateNativeVerificationFlow(ctx context.Context) FrontendAPIApiCreateNativeVerificationFlowRequest { + return FrontendAPIApiCreateNativeVerificationFlowRequest{ ApiService: a, ctx: ctx, } @@ -2537,7 +2545,7 @@ func (a *FrontendApiService) CreateNativeVerificationFlow(ctx context.Context) F * Execute executes the request * @return VerificationFlow */ -func (a *FrontendApiService) CreateNativeVerificationFlowExecute(r FrontendApiApiCreateNativeVerificationFlowRequest) (*VerificationFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateNativeVerificationFlowExecute(r FrontendAPIApiCreateNativeVerificationFlowRequest) (*VerificationFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2547,7 +2555,7 @@ func (a *FrontendApiService) CreateNativeVerificationFlowExecute(r FrontendApiAp localVarReturnValue *VerificationFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateNativeVerificationFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateNativeVerificationFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2585,7 +2593,7 @@ func (a *FrontendApiService) CreateNativeVerificationFlowExecute(r FrontendApiAp return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2629,23 +2637,23 @@ func (a *FrontendApiService) CreateNativeVerificationFlowExecute(r FrontendApiAp return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiDisableMyOtherSessionsRequest struct { +type FrontendAPIApiDisableMyOtherSessionsRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI xSessionToken *string cookie *string } -func (r FrontendApiApiDisableMyOtherSessionsRequest) XSessionToken(xSessionToken string) FrontendApiApiDisableMyOtherSessionsRequest { +func (r FrontendAPIApiDisableMyOtherSessionsRequest) XSessionToken(xSessionToken string) FrontendAPIApiDisableMyOtherSessionsRequest { r.xSessionToken = &xSessionToken return r } -func (r FrontendApiApiDisableMyOtherSessionsRequest) Cookie(cookie string) FrontendApiApiDisableMyOtherSessionsRequest { +func (r FrontendAPIApiDisableMyOtherSessionsRequest) Cookie(cookie string) FrontendAPIApiDisableMyOtherSessionsRequest { r.cookie = &cookie return r } -func (r FrontendApiApiDisableMyOtherSessionsRequest) Execute() (*DeleteMySessionsCount, *http.Response, error) { +func (r FrontendAPIApiDisableMyOtherSessionsRequest) Execute() (*DeleteMySessionsCount, *http.Response, error) { return r.ApiService.DisableMyOtherSessionsExecute(r) } @@ -2655,10 +2663,10 @@ func (r FrontendApiApiDisableMyOtherSessionsRequest) Execute() (*DeleteMySession Session data are not deleted. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiDisableMyOtherSessionsRequest + - @return FrontendAPIApiDisableMyOtherSessionsRequest */ -func (a *FrontendApiService) DisableMyOtherSessions(ctx context.Context) FrontendApiApiDisableMyOtherSessionsRequest { - return FrontendApiApiDisableMyOtherSessionsRequest{ +func (a *FrontendAPIService) DisableMyOtherSessions(ctx context.Context) FrontendAPIApiDisableMyOtherSessionsRequest { + return FrontendAPIApiDisableMyOtherSessionsRequest{ ApiService: a, ctx: ctx, } @@ -2668,7 +2676,7 @@ func (a *FrontendApiService) DisableMyOtherSessions(ctx context.Context) Fronten * Execute executes the request * @return DeleteMySessionsCount */ -func (a *FrontendApiService) DisableMyOtherSessionsExecute(r FrontendApiApiDisableMyOtherSessionsRequest) (*DeleteMySessionsCount, *http.Response, error) { +func (a *FrontendAPIService) DisableMyOtherSessionsExecute(r FrontendAPIApiDisableMyOtherSessionsRequest) (*DeleteMySessionsCount, *http.Response, error) { var ( localVarHTTPMethod = http.MethodDelete localVarPostBody interface{} @@ -2678,7 +2686,7 @@ func (a *FrontendApiService) DisableMyOtherSessionsExecute(r FrontendApiApiDisab localVarReturnValue *DeleteMySessionsCount ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.DisableMyOtherSessions") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.DisableMyOtherSessions") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2722,7 +2730,7 @@ func (a *FrontendApiService) DisableMyOtherSessionsExecute(r FrontendApiApiDisab return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2776,24 +2784,24 @@ func (a *FrontendApiService) DisableMyOtherSessionsExecute(r FrontendApiApiDisab return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiDisableMySessionRequest struct { +type FrontendAPIApiDisableMySessionRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI id string xSessionToken *string cookie *string } -func (r FrontendApiApiDisableMySessionRequest) XSessionToken(xSessionToken string) FrontendApiApiDisableMySessionRequest { +func (r FrontendAPIApiDisableMySessionRequest) XSessionToken(xSessionToken string) FrontendAPIApiDisableMySessionRequest { r.xSessionToken = &xSessionToken return r } -func (r FrontendApiApiDisableMySessionRequest) Cookie(cookie string) FrontendApiApiDisableMySessionRequest { +func (r FrontendAPIApiDisableMySessionRequest) Cookie(cookie string) FrontendAPIApiDisableMySessionRequest { r.cookie = &cookie return r } -func (r FrontendApiApiDisableMySessionRequest) Execute() (*http.Response, error) { +func (r FrontendAPIApiDisableMySessionRequest) Execute() (*http.Response, error) { return r.ApiService.DisableMySessionExecute(r) } @@ -2804,10 +2812,10 @@ func (r FrontendApiApiDisableMySessionRequest) Execute() (*http.Response, error) Session data are not deleted. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the session's ID. - - @return FrontendApiApiDisableMySessionRequest + - @return FrontendAPIApiDisableMySessionRequest */ -func (a *FrontendApiService) DisableMySession(ctx context.Context, id string) FrontendApiApiDisableMySessionRequest { - return FrontendApiApiDisableMySessionRequest{ +func (a *FrontendAPIService) DisableMySession(ctx context.Context, id string) FrontendAPIApiDisableMySessionRequest { + return FrontendAPIApiDisableMySessionRequest{ ApiService: a, ctx: ctx, id: id, @@ -2817,7 +2825,7 @@ func (a *FrontendApiService) DisableMySession(ctx context.Context, id string) Fr /* * Execute executes the request */ -func (a *FrontendApiService) DisableMySessionExecute(r FrontendApiApiDisableMySessionRequest) (*http.Response, error) { +func (a *FrontendAPIService) DisableMySessionExecute(r FrontendAPIApiDisableMySessionRequest) (*http.Response, error) { var ( localVarHTTPMethod = http.MethodDelete localVarPostBody interface{} @@ -2826,7 +2834,7 @@ func (a *FrontendApiService) DisableMySessionExecute(r FrontendApiApiDisableMySe localVarFileBytes []byte ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.DisableMySession") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.DisableMySession") if err != nil { return nil, &GenericOpenAPIError{error: err.Error()} } @@ -2871,7 +2879,7 @@ func (a *FrontendApiService) DisableMySessionExecute(r FrontendApiApiDisableMySe return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2916,33 +2924,33 @@ func (a *FrontendApiService) DisableMySessionExecute(r FrontendApiApiDisableMySe return localVarHTTPResponse, nil } -type FrontendApiApiExchangeSessionTokenRequest struct { +type FrontendAPIApiExchangeSessionTokenRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI initCode *string returnToCode *string } -func (r FrontendApiApiExchangeSessionTokenRequest) InitCode(initCode string) FrontendApiApiExchangeSessionTokenRequest { +func (r FrontendAPIApiExchangeSessionTokenRequest) InitCode(initCode string) FrontendAPIApiExchangeSessionTokenRequest { r.initCode = &initCode return r } -func (r FrontendApiApiExchangeSessionTokenRequest) ReturnToCode(returnToCode string) FrontendApiApiExchangeSessionTokenRequest { +func (r FrontendAPIApiExchangeSessionTokenRequest) ReturnToCode(returnToCode string) FrontendAPIApiExchangeSessionTokenRequest { r.returnToCode = &returnToCode return r } -func (r FrontendApiApiExchangeSessionTokenRequest) Execute() (*SuccessfulNativeLogin, *http.Response, error) { +func (r FrontendAPIApiExchangeSessionTokenRequest) Execute() (*SuccessfulNativeLogin, *http.Response, error) { return r.ApiService.ExchangeSessionTokenExecute(r) } /* * ExchangeSessionToken Exchange Session Token * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiExchangeSessionTokenRequest + * @return FrontendAPIApiExchangeSessionTokenRequest */ -func (a *FrontendApiService) ExchangeSessionToken(ctx context.Context) FrontendApiApiExchangeSessionTokenRequest { - return FrontendApiApiExchangeSessionTokenRequest{ +func (a *FrontendAPIService) ExchangeSessionToken(ctx context.Context) FrontendAPIApiExchangeSessionTokenRequest { + return FrontendAPIApiExchangeSessionTokenRequest{ ApiService: a, ctx: ctx, } @@ -2952,7 +2960,7 @@ func (a *FrontendApiService) ExchangeSessionToken(ctx context.Context) FrontendA * Execute executes the request * @return SuccessfulNativeLogin */ -func (a *FrontendApiService) ExchangeSessionTokenExecute(r FrontendApiApiExchangeSessionTokenRequest) (*SuccessfulNativeLogin, *http.Response, error) { +func (a *FrontendAPIService) ExchangeSessionTokenExecute(r FrontendAPIApiExchangeSessionTokenRequest) (*SuccessfulNativeLogin, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2962,7 +2970,7 @@ func (a *FrontendApiService) ExchangeSessionTokenExecute(r FrontendApiApiExchang localVarReturnValue *SuccessfulNativeLogin ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.ExchangeSessionToken") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.ExchangeSessionToken") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3008,7 +3016,7 @@ func (a *FrontendApiService) ExchangeSessionTokenExecute(r FrontendApiApiExchang return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3072,18 +3080,18 @@ func (a *FrontendApiService) ExchangeSessionTokenExecute(r FrontendApiApiExchang return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiGetFlowErrorRequest struct { +type FrontendAPIApiGetFlowErrorRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI id *string } -func (r FrontendApiApiGetFlowErrorRequest) Id(id string) FrontendApiApiGetFlowErrorRequest { +func (r FrontendAPIApiGetFlowErrorRequest) Id(id string) FrontendAPIApiGetFlowErrorRequest { r.id = &id return r } -func (r FrontendApiApiGetFlowErrorRequest) Execute() (*FlowError, *http.Response, error) { +func (r FrontendAPIApiGetFlowErrorRequest) Execute() (*FlowError, *http.Response, error) { return r.ApiService.GetFlowErrorExecute(r) } @@ -3097,10 +3105,10 @@ This endpoint supports stub values to help you implement the error UI: More information can be found at [Ory Kratos User User Facing Error Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-facing-errors). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiGetFlowErrorRequest + - @return FrontendAPIApiGetFlowErrorRequest */ -func (a *FrontendApiService) GetFlowError(ctx context.Context) FrontendApiApiGetFlowErrorRequest { - return FrontendApiApiGetFlowErrorRequest{ +func (a *FrontendAPIService) GetFlowError(ctx context.Context) FrontendAPIApiGetFlowErrorRequest { + return FrontendAPIApiGetFlowErrorRequest{ ApiService: a, ctx: ctx, } @@ -3110,7 +3118,7 @@ func (a *FrontendApiService) GetFlowError(ctx context.Context) FrontendApiApiGet * Execute executes the request * @return FlowError */ -func (a *FrontendApiService) GetFlowErrorExecute(r FrontendApiApiGetFlowErrorRequest) (*FlowError, *http.Response, error) { +func (a *FrontendAPIService) GetFlowErrorExecute(r FrontendAPIApiGetFlowErrorRequest) (*FlowError, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -3120,7 +3128,7 @@ func (a *FrontendApiService) GetFlowErrorExecute(r FrontendApiApiGetFlowErrorReq localVarReturnValue *FlowError ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.GetFlowError") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.GetFlowError") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3162,7 +3170,7 @@ func (a *FrontendApiService) GetFlowErrorExecute(r FrontendApiApiGetFlowErrorReq return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3218,23 +3226,23 @@ func (a *FrontendApiService) GetFlowErrorExecute(r FrontendApiApiGetFlowErrorReq return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiGetLoginFlowRequest struct { +type FrontendAPIApiGetLoginFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI id *string cookie *string } -func (r FrontendApiApiGetLoginFlowRequest) Id(id string) FrontendApiApiGetLoginFlowRequest { +func (r FrontendAPIApiGetLoginFlowRequest) Id(id string) FrontendAPIApiGetLoginFlowRequest { r.id = &id return r } -func (r FrontendApiApiGetLoginFlowRequest) Cookie(cookie string) FrontendApiApiGetLoginFlowRequest { +func (r FrontendAPIApiGetLoginFlowRequest) Cookie(cookie string) FrontendAPIApiGetLoginFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiGetLoginFlowRequest) Execute() (*LoginFlow, *http.Response, error) { +func (r FrontendAPIApiGetLoginFlowRequest) Execute() (*LoginFlow, *http.Response, error) { return r.ApiService.GetLoginFlowExecute(r) } @@ -3264,10 +3272,10 @@ This request may fail due to several reasons. The `error.id` can be one of: More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiGetLoginFlowRequest + - @return FrontendAPIApiGetLoginFlowRequest */ -func (a *FrontendApiService) GetLoginFlow(ctx context.Context) FrontendApiApiGetLoginFlowRequest { - return FrontendApiApiGetLoginFlowRequest{ +func (a *FrontendAPIService) GetLoginFlow(ctx context.Context) FrontendAPIApiGetLoginFlowRequest { + return FrontendAPIApiGetLoginFlowRequest{ ApiService: a, ctx: ctx, } @@ -3277,7 +3285,7 @@ func (a *FrontendApiService) GetLoginFlow(ctx context.Context) FrontendApiApiGet * Execute executes the request * @return LoginFlow */ -func (a *FrontendApiService) GetLoginFlowExecute(r FrontendApiApiGetLoginFlowRequest) (*LoginFlow, *http.Response, error) { +func (a *FrontendAPIService) GetLoginFlowExecute(r FrontendAPIApiGetLoginFlowRequest) (*LoginFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -3287,7 +3295,7 @@ func (a *FrontendApiService) GetLoginFlowExecute(r FrontendApiApiGetLoginFlowReq localVarReturnValue *LoginFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.GetLoginFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.GetLoginFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3332,7 +3340,7 @@ func (a *FrontendApiService) GetLoginFlowExecute(r FrontendApiApiGetLoginFlowReq return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3396,23 +3404,23 @@ func (a *FrontendApiService) GetLoginFlowExecute(r FrontendApiApiGetLoginFlowReq return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiGetRecoveryFlowRequest struct { +type FrontendAPIApiGetRecoveryFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI id *string cookie *string } -func (r FrontendApiApiGetRecoveryFlowRequest) Id(id string) FrontendApiApiGetRecoveryFlowRequest { +func (r FrontendAPIApiGetRecoveryFlowRequest) Id(id string) FrontendAPIApiGetRecoveryFlowRequest { r.id = &id return r } -func (r FrontendApiApiGetRecoveryFlowRequest) Cookie(cookie string) FrontendApiApiGetRecoveryFlowRequest { +func (r FrontendAPIApiGetRecoveryFlowRequest) Cookie(cookie string) FrontendAPIApiGetRecoveryFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiGetRecoveryFlowRequest) Execute() (*RecoveryFlow, *http.Response, error) { +func (r FrontendAPIApiGetRecoveryFlowRequest) Execute() (*RecoveryFlow, *http.Response, error) { return r.ApiService.GetRecoveryFlowExecute(r) } @@ -3437,10 +3445,10 @@ res.render('recovery', flow) More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiGetRecoveryFlowRequest + - @return FrontendAPIApiGetRecoveryFlowRequest */ -func (a *FrontendApiService) GetRecoveryFlow(ctx context.Context) FrontendApiApiGetRecoveryFlowRequest { - return FrontendApiApiGetRecoveryFlowRequest{ +func (a *FrontendAPIService) GetRecoveryFlow(ctx context.Context) FrontendAPIApiGetRecoveryFlowRequest { + return FrontendAPIApiGetRecoveryFlowRequest{ ApiService: a, ctx: ctx, } @@ -3450,7 +3458,7 @@ func (a *FrontendApiService) GetRecoveryFlow(ctx context.Context) FrontendApiApi * Execute executes the request * @return RecoveryFlow */ -func (a *FrontendApiService) GetRecoveryFlowExecute(r FrontendApiApiGetRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) { +func (a *FrontendAPIService) GetRecoveryFlowExecute(r FrontendAPIApiGetRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -3460,7 +3468,7 @@ func (a *FrontendApiService) GetRecoveryFlowExecute(r FrontendApiApiGetRecoveryF localVarReturnValue *RecoveryFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.GetRecoveryFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.GetRecoveryFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3505,7 +3513,7 @@ func (a *FrontendApiService) GetRecoveryFlowExecute(r FrontendApiApiGetRecoveryF return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3559,23 +3567,23 @@ func (a *FrontendApiService) GetRecoveryFlowExecute(r FrontendApiApiGetRecoveryF return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiGetRegistrationFlowRequest struct { +type FrontendAPIApiGetRegistrationFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI id *string cookie *string } -func (r FrontendApiApiGetRegistrationFlowRequest) Id(id string) FrontendApiApiGetRegistrationFlowRequest { +func (r FrontendAPIApiGetRegistrationFlowRequest) Id(id string) FrontendAPIApiGetRegistrationFlowRequest { r.id = &id return r } -func (r FrontendApiApiGetRegistrationFlowRequest) Cookie(cookie string) FrontendApiApiGetRegistrationFlowRequest { +func (r FrontendAPIApiGetRegistrationFlowRequest) Cookie(cookie string) FrontendAPIApiGetRegistrationFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiGetRegistrationFlowRequest) Execute() (*RegistrationFlow, *http.Response, error) { +func (r FrontendAPIApiGetRegistrationFlowRequest) Execute() (*RegistrationFlow, *http.Response, error) { return r.ApiService.GetRegistrationFlowExecute(r) } @@ -3605,10 +3613,10 @@ This request may fail due to several reasons. The `error.id` can be one of: More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiGetRegistrationFlowRequest + - @return FrontendAPIApiGetRegistrationFlowRequest */ -func (a *FrontendApiService) GetRegistrationFlow(ctx context.Context) FrontendApiApiGetRegistrationFlowRequest { - return FrontendApiApiGetRegistrationFlowRequest{ +func (a *FrontendAPIService) GetRegistrationFlow(ctx context.Context) FrontendAPIApiGetRegistrationFlowRequest { + return FrontendAPIApiGetRegistrationFlowRequest{ ApiService: a, ctx: ctx, } @@ -3618,7 +3626,7 @@ func (a *FrontendApiService) GetRegistrationFlow(ctx context.Context) FrontendAp * Execute executes the request * @return RegistrationFlow */ -func (a *FrontendApiService) GetRegistrationFlowExecute(r FrontendApiApiGetRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) { +func (a *FrontendAPIService) GetRegistrationFlowExecute(r FrontendAPIApiGetRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -3628,7 +3636,7 @@ func (a *FrontendApiService) GetRegistrationFlowExecute(r FrontendApiApiGetRegis localVarReturnValue *RegistrationFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.GetRegistrationFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.GetRegistrationFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3673,7 +3681,7 @@ func (a *FrontendApiService) GetRegistrationFlowExecute(r FrontendApiApiGetRegis return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3737,28 +3745,28 @@ func (a *FrontendApiService) GetRegistrationFlowExecute(r FrontendApiApiGetRegis return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiGetSettingsFlowRequest struct { +type FrontendAPIApiGetSettingsFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI id *string xSessionToken *string cookie *string } -func (r FrontendApiApiGetSettingsFlowRequest) Id(id string) FrontendApiApiGetSettingsFlowRequest { +func (r FrontendAPIApiGetSettingsFlowRequest) Id(id string) FrontendAPIApiGetSettingsFlowRequest { r.id = &id return r } -func (r FrontendApiApiGetSettingsFlowRequest) XSessionToken(xSessionToken string) FrontendApiApiGetSettingsFlowRequest { +func (r FrontendAPIApiGetSettingsFlowRequest) XSessionToken(xSessionToken string) FrontendAPIApiGetSettingsFlowRequest { r.xSessionToken = &xSessionToken return r } -func (r FrontendApiApiGetSettingsFlowRequest) Cookie(cookie string) FrontendApiApiGetSettingsFlowRequest { +func (r FrontendAPIApiGetSettingsFlowRequest) Cookie(cookie string) FrontendAPIApiGetSettingsFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiGetSettingsFlowRequest) Execute() (*SettingsFlow, *http.Response, error) { +func (r FrontendAPIApiGetSettingsFlowRequest) Execute() (*SettingsFlow, *http.Response, error) { return r.ApiService.GetSettingsFlowExecute(r) } @@ -3785,10 +3793,10 @@ identity logged in instead. More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiGetSettingsFlowRequest + - @return FrontendAPIApiGetSettingsFlowRequest */ -func (a *FrontendApiService) GetSettingsFlow(ctx context.Context) FrontendApiApiGetSettingsFlowRequest { - return FrontendApiApiGetSettingsFlowRequest{ +func (a *FrontendAPIService) GetSettingsFlow(ctx context.Context) FrontendAPIApiGetSettingsFlowRequest { + return FrontendAPIApiGetSettingsFlowRequest{ ApiService: a, ctx: ctx, } @@ -3798,7 +3806,7 @@ func (a *FrontendApiService) GetSettingsFlow(ctx context.Context) FrontendApiApi * Execute executes the request * @return SettingsFlow */ -func (a *FrontendApiService) GetSettingsFlowExecute(r FrontendApiApiGetSettingsFlowRequest) (*SettingsFlow, *http.Response, error) { +func (a *FrontendAPIService) GetSettingsFlowExecute(r FrontendAPIApiGetSettingsFlowRequest) (*SettingsFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -3808,7 +3816,7 @@ func (a *FrontendApiService) GetSettingsFlowExecute(r FrontendApiApiGetSettingsF localVarReturnValue *SettingsFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.GetSettingsFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.GetSettingsFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3856,7 +3864,7 @@ func (a *FrontendApiService) GetSettingsFlowExecute(r FrontendApiApiGetSettingsF return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3930,23 +3938,23 @@ func (a *FrontendApiService) GetSettingsFlowExecute(r FrontendApiApiGetSettingsF return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiGetVerificationFlowRequest struct { +type FrontendAPIApiGetVerificationFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI id *string cookie *string } -func (r FrontendApiApiGetVerificationFlowRequest) Id(id string) FrontendApiApiGetVerificationFlowRequest { +func (r FrontendAPIApiGetVerificationFlowRequest) Id(id string) FrontendAPIApiGetVerificationFlowRequest { r.id = &id return r } -func (r FrontendApiApiGetVerificationFlowRequest) Cookie(cookie string) FrontendApiApiGetVerificationFlowRequest { +func (r FrontendAPIApiGetVerificationFlowRequest) Cookie(cookie string) FrontendAPIApiGetVerificationFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiGetVerificationFlowRequest) Execute() (*VerificationFlow, *http.Response, error) { +func (r FrontendAPIApiGetVerificationFlowRequest) Execute() (*VerificationFlow, *http.Response, error) { return r.ApiService.GetVerificationFlowExecute(r) } @@ -3971,10 +3979,10 @@ res.render('verification', flow) More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiGetVerificationFlowRequest + - @return FrontendAPIApiGetVerificationFlowRequest */ -func (a *FrontendApiService) GetVerificationFlow(ctx context.Context) FrontendApiApiGetVerificationFlowRequest { - return FrontendApiApiGetVerificationFlowRequest{ +func (a *FrontendAPIService) GetVerificationFlow(ctx context.Context) FrontendAPIApiGetVerificationFlowRequest { + return FrontendAPIApiGetVerificationFlowRequest{ ApiService: a, ctx: ctx, } @@ -3984,7 +3992,7 @@ func (a *FrontendApiService) GetVerificationFlow(ctx context.Context) FrontendAp * Execute executes the request * @return VerificationFlow */ -func (a *FrontendApiService) GetVerificationFlowExecute(r FrontendApiApiGetVerificationFlowRequest) (*VerificationFlow, *http.Response, error) { +func (a *FrontendAPIService) GetVerificationFlowExecute(r FrontendAPIApiGetVerificationFlowRequest) (*VerificationFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -3994,7 +4002,7 @@ func (a *FrontendApiService) GetVerificationFlowExecute(r FrontendApiApiGetVerif localVarReturnValue *VerificationFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.GetVerificationFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.GetVerificationFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -4039,7 +4047,7 @@ func (a *FrontendApiService) GetVerificationFlowExecute(r FrontendApiApiGetVerif return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4093,12 +4101,12 @@ func (a *FrontendApiService) GetVerificationFlowExecute(r FrontendApiApiGetVerif return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiGetWebAuthnJavaScriptRequest struct { +type FrontendAPIApiGetWebAuthnJavaScriptRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI } -func (r FrontendApiApiGetWebAuthnJavaScriptRequest) Execute() (string, *http.Response, error) { +func (r FrontendAPIApiGetWebAuthnJavaScriptRequest) Execute() (string, *http.Response, error) { return r.ApiService.GetWebAuthnJavaScriptExecute(r) } @@ -4114,10 +4122,10 @@ If you are building a JavaScript Browser App (e.g. in ReactJS or AngularJS) you More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiGetWebAuthnJavaScriptRequest + - @return FrontendAPIApiGetWebAuthnJavaScriptRequest */ -func (a *FrontendApiService) GetWebAuthnJavaScript(ctx context.Context) FrontendApiApiGetWebAuthnJavaScriptRequest { - return FrontendApiApiGetWebAuthnJavaScriptRequest{ +func (a *FrontendAPIService) GetWebAuthnJavaScript(ctx context.Context) FrontendAPIApiGetWebAuthnJavaScriptRequest { + return FrontendAPIApiGetWebAuthnJavaScriptRequest{ ApiService: a, ctx: ctx, } @@ -4127,7 +4135,7 @@ func (a *FrontendApiService) GetWebAuthnJavaScript(ctx context.Context) Frontend * Execute executes the request * @return string */ -func (a *FrontendApiService) GetWebAuthnJavaScriptExecute(r FrontendApiApiGetWebAuthnJavaScriptRequest) (string, *http.Response, error) { +func (a *FrontendAPIService) GetWebAuthnJavaScriptExecute(r FrontendAPIApiGetWebAuthnJavaScriptRequest) (string, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -4137,7 +4145,7 @@ func (a *FrontendApiService) GetWebAuthnJavaScriptExecute(r FrontendApiApiGetWeb localVarReturnValue string ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.GetWebAuthnJavaScript") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.GetWebAuthnJavaScript") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -4175,7 +4183,7 @@ func (a *FrontendApiService) GetWebAuthnJavaScriptExecute(r FrontendApiApiGetWeb return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4202,9 +4210,9 @@ func (a *FrontendApiService) GetWebAuthnJavaScriptExecute(r FrontendApiApiGetWeb return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiListMySessionsRequest struct { +type FrontendAPIApiListMySessionsRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI perPage *int64 page *int64 pageSize *int64 @@ -4213,32 +4221,32 @@ type FrontendApiApiListMySessionsRequest struct { cookie *string } -func (r FrontendApiApiListMySessionsRequest) PerPage(perPage int64) FrontendApiApiListMySessionsRequest { +func (r FrontendAPIApiListMySessionsRequest) PerPage(perPage int64) FrontendAPIApiListMySessionsRequest { r.perPage = &perPage return r } -func (r FrontendApiApiListMySessionsRequest) Page(page int64) FrontendApiApiListMySessionsRequest { +func (r FrontendAPIApiListMySessionsRequest) Page(page int64) FrontendAPIApiListMySessionsRequest { r.page = &page return r } -func (r FrontendApiApiListMySessionsRequest) PageSize(pageSize int64) FrontendApiApiListMySessionsRequest { +func (r FrontendAPIApiListMySessionsRequest) PageSize(pageSize int64) FrontendAPIApiListMySessionsRequest { r.pageSize = &pageSize return r } -func (r FrontendApiApiListMySessionsRequest) PageToken(pageToken string) FrontendApiApiListMySessionsRequest { +func (r FrontendAPIApiListMySessionsRequest) PageToken(pageToken string) FrontendAPIApiListMySessionsRequest { r.pageToken = &pageToken return r } -func (r FrontendApiApiListMySessionsRequest) XSessionToken(xSessionToken string) FrontendApiApiListMySessionsRequest { +func (r FrontendAPIApiListMySessionsRequest) XSessionToken(xSessionToken string) FrontendAPIApiListMySessionsRequest { r.xSessionToken = &xSessionToken return r } -func (r FrontendApiApiListMySessionsRequest) Cookie(cookie string) FrontendApiApiListMySessionsRequest { +func (r FrontendAPIApiListMySessionsRequest) Cookie(cookie string) FrontendAPIApiListMySessionsRequest { r.cookie = &cookie return r } -func (r FrontendApiApiListMySessionsRequest) Execute() ([]Session, *http.Response, error) { +func (r FrontendAPIApiListMySessionsRequest) Execute() ([]Session, *http.Response, error) { return r.ApiService.ListMySessionsExecute(r) } @@ -4248,10 +4256,10 @@ func (r FrontendApiApiListMySessionsRequest) Execute() ([]Session, *http.Respons The current session can be retrieved by calling the `/sessions/whoami` endpoint. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiListMySessionsRequest + - @return FrontendAPIApiListMySessionsRequest */ -func (a *FrontendApiService) ListMySessions(ctx context.Context) FrontendApiApiListMySessionsRequest { - return FrontendApiApiListMySessionsRequest{ +func (a *FrontendAPIService) ListMySessions(ctx context.Context) FrontendAPIApiListMySessionsRequest { + return FrontendAPIApiListMySessionsRequest{ ApiService: a, ctx: ctx, } @@ -4261,7 +4269,7 @@ func (a *FrontendApiService) ListMySessions(ctx context.Context) FrontendApiApiL * Execute executes the request * @return []Session */ -func (a *FrontendApiService) ListMySessionsExecute(r FrontendApiApiListMySessionsRequest) ([]Session, *http.Response, error) { +func (a *FrontendAPIService) ListMySessionsExecute(r FrontendAPIApiListMySessionsRequest) ([]Session, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -4271,7 +4279,7 @@ func (a *FrontendApiService) ListMySessionsExecute(r FrontendApiApiListMySession localVarReturnValue []Session ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.ListMySessions") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.ListMySessions") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -4327,7 +4335,7 @@ func (a *FrontendApiService) ListMySessionsExecute(r FrontendApiApiListMySession return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4381,18 +4389,18 @@ func (a *FrontendApiService) ListMySessionsExecute(r FrontendApiApiListMySession return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiPerformNativeLogoutRequest struct { +type FrontendAPIApiPerformNativeLogoutRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI performNativeLogoutBody *PerformNativeLogoutBody } -func (r FrontendApiApiPerformNativeLogoutRequest) PerformNativeLogoutBody(performNativeLogoutBody PerformNativeLogoutBody) FrontendApiApiPerformNativeLogoutRequest { +func (r FrontendAPIApiPerformNativeLogoutRequest) PerformNativeLogoutBody(performNativeLogoutBody PerformNativeLogoutBody) FrontendAPIApiPerformNativeLogoutRequest { r.performNativeLogoutBody = &performNativeLogoutBody return r } -func (r FrontendApiApiPerformNativeLogoutRequest) Execute() (*http.Response, error) { +func (r FrontendAPIApiPerformNativeLogoutRequest) Execute() (*http.Response, error) { return r.ApiService.PerformNativeLogoutExecute(r) } @@ -4408,10 +4416,10 @@ If the Ory Session Token is malformed or does not exist a 403 Forbidden response This endpoint does not remove any HTTP Cookies - use the Browser-Based Self-Service Logout Flow instead. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiPerformNativeLogoutRequest + - @return FrontendAPIApiPerformNativeLogoutRequest */ -func (a *FrontendApiService) PerformNativeLogout(ctx context.Context) FrontendApiApiPerformNativeLogoutRequest { - return FrontendApiApiPerformNativeLogoutRequest{ +func (a *FrontendAPIService) PerformNativeLogout(ctx context.Context) FrontendAPIApiPerformNativeLogoutRequest { + return FrontendAPIApiPerformNativeLogoutRequest{ ApiService: a, ctx: ctx, } @@ -4420,7 +4428,7 @@ func (a *FrontendApiService) PerformNativeLogout(ctx context.Context) FrontendAp /* * Execute executes the request */ -func (a *FrontendApiService) PerformNativeLogoutExecute(r FrontendApiApiPerformNativeLogoutRequest) (*http.Response, error) { +func (a *FrontendAPIService) PerformNativeLogoutExecute(r FrontendAPIApiPerformNativeLogoutRequest) (*http.Response, error) { var ( localVarHTTPMethod = http.MethodDelete localVarPostBody interface{} @@ -4429,7 +4437,7 @@ func (a *FrontendApiService) PerformNativeLogoutExecute(r FrontendApiApiPerformN localVarFileBytes []byte ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.PerformNativeLogout") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.PerformNativeLogout") if err != nil { return nil, &GenericOpenAPIError{error: err.Error()} } @@ -4472,7 +4480,7 @@ func (a *FrontendApiService) PerformNativeLogoutExecute(r FrontendApiApiPerformN return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4507,28 +4515,28 @@ func (a *FrontendApiService) PerformNativeLogoutExecute(r FrontendApiApiPerformN return localVarHTTPResponse, nil } -type FrontendApiApiToSessionRequest struct { +type FrontendAPIApiToSessionRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI xSessionToken *string cookie *string tokenizeAs *string } -func (r FrontendApiApiToSessionRequest) XSessionToken(xSessionToken string) FrontendApiApiToSessionRequest { +func (r FrontendAPIApiToSessionRequest) XSessionToken(xSessionToken string) FrontendAPIApiToSessionRequest { r.xSessionToken = &xSessionToken return r } -func (r FrontendApiApiToSessionRequest) Cookie(cookie string) FrontendApiApiToSessionRequest { +func (r FrontendAPIApiToSessionRequest) Cookie(cookie string) FrontendAPIApiToSessionRequest { r.cookie = &cookie return r } -func (r FrontendApiApiToSessionRequest) TokenizeAs(tokenizeAs string) FrontendApiApiToSessionRequest { +func (r FrontendAPIApiToSessionRequest) TokenizeAs(tokenizeAs string) FrontendAPIApiToSessionRequest { r.tokenizeAs = &tokenizeAs return r } -func (r FrontendApiApiToSessionRequest) Execute() (*Session, *http.Response, error) { +func (r FrontendAPIApiToSessionRequest) Execute() (*Session, *http.Response, error) { return r.ApiService.ToSessionExecute(r) } @@ -4596,10 +4604,10 @@ As explained above, this request may fail due to several reasons. The `error.id` `session_inactive`: No active session was found in the request (e.g. no Ory Session Cookie / Ory Session Token). `session_aal2_required`: An active session was found but it does not fulfil the Authenticator Assurance Level, implying that the session must (e.g.) authenticate the second factor. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiToSessionRequest + - @return FrontendAPIApiToSessionRequest */ -func (a *FrontendApiService) ToSession(ctx context.Context) FrontendApiApiToSessionRequest { - return FrontendApiApiToSessionRequest{ +func (a *FrontendAPIService) ToSession(ctx context.Context) FrontendAPIApiToSessionRequest { + return FrontendAPIApiToSessionRequest{ ApiService: a, ctx: ctx, } @@ -4609,7 +4617,7 @@ func (a *FrontendApiService) ToSession(ctx context.Context) FrontendApiApiToSess * Execute executes the request * @return Session */ -func (a *FrontendApiService) ToSessionExecute(r FrontendApiApiToSessionRequest) (*Session, *http.Response, error) { +func (a *FrontendAPIService) ToSessionExecute(r FrontendAPIApiToSessionRequest) (*Session, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -4619,7 +4627,7 @@ func (a *FrontendApiService) ToSessionExecute(r FrontendApiApiToSessionRequest) localVarReturnValue *Session ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.ToSession") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.ToSession") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -4666,7 +4674,7 @@ func (a *FrontendApiService) ToSessionExecute(r FrontendApiApiToSessionRequest) return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4720,33 +4728,33 @@ func (a *FrontendApiService) ToSessionExecute(r FrontendApiApiToSessionRequest) return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiUpdateLoginFlowRequest struct { +type FrontendAPIApiUpdateLoginFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI flow *string updateLoginFlowBody *UpdateLoginFlowBody xSessionToken *string cookie *string } -func (r FrontendApiApiUpdateLoginFlowRequest) Flow(flow string) FrontendApiApiUpdateLoginFlowRequest { +func (r FrontendAPIApiUpdateLoginFlowRequest) Flow(flow string) FrontendAPIApiUpdateLoginFlowRequest { r.flow = &flow return r } -func (r FrontendApiApiUpdateLoginFlowRequest) UpdateLoginFlowBody(updateLoginFlowBody UpdateLoginFlowBody) FrontendApiApiUpdateLoginFlowRequest { +func (r FrontendAPIApiUpdateLoginFlowRequest) UpdateLoginFlowBody(updateLoginFlowBody UpdateLoginFlowBody) FrontendAPIApiUpdateLoginFlowRequest { r.updateLoginFlowBody = &updateLoginFlowBody return r } -func (r FrontendApiApiUpdateLoginFlowRequest) XSessionToken(xSessionToken string) FrontendApiApiUpdateLoginFlowRequest { +func (r FrontendAPIApiUpdateLoginFlowRequest) XSessionToken(xSessionToken string) FrontendAPIApiUpdateLoginFlowRequest { r.xSessionToken = &xSessionToken return r } -func (r FrontendApiApiUpdateLoginFlowRequest) Cookie(cookie string) FrontendApiApiUpdateLoginFlowRequest { +func (r FrontendAPIApiUpdateLoginFlowRequest) Cookie(cookie string) FrontendAPIApiUpdateLoginFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiUpdateLoginFlowRequest) Execute() (*SuccessfulNativeLogin, *http.Response, error) { +func (r FrontendAPIApiUpdateLoginFlowRequest) Execute() (*SuccessfulNativeLogin, *http.Response, error) { return r.ApiService.UpdateLoginFlowExecute(r) } @@ -4781,10 +4789,10 @@ Most likely used in Social Sign In flows. More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiUpdateLoginFlowRequest + - @return FrontendAPIApiUpdateLoginFlowRequest */ -func (a *FrontendApiService) UpdateLoginFlow(ctx context.Context) FrontendApiApiUpdateLoginFlowRequest { - return FrontendApiApiUpdateLoginFlowRequest{ +func (a *FrontendAPIService) UpdateLoginFlow(ctx context.Context) FrontendAPIApiUpdateLoginFlowRequest { + return FrontendAPIApiUpdateLoginFlowRequest{ ApiService: a, ctx: ctx, } @@ -4794,7 +4802,7 @@ func (a *FrontendApiService) UpdateLoginFlow(ctx context.Context) FrontendApiApi * Execute executes the request * @return SuccessfulNativeLogin */ -func (a *FrontendApiService) UpdateLoginFlowExecute(r FrontendApiApiUpdateLoginFlowRequest) (*SuccessfulNativeLogin, *http.Response, error) { +func (a *FrontendAPIService) UpdateLoginFlowExecute(r FrontendAPIApiUpdateLoginFlowRequest) (*SuccessfulNativeLogin, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPost localVarPostBody interface{} @@ -4804,7 +4812,7 @@ func (a *FrontendApiService) UpdateLoginFlowExecute(r FrontendApiApiUpdateLoginF localVarReturnValue *SuccessfulNativeLogin ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.UpdateLoginFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.UpdateLoginFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -4857,7 +4865,7 @@ func (a *FrontendApiService) UpdateLoginFlowExecute(r FrontendApiApiUpdateLoginF return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4921,28 +4929,28 @@ func (a *FrontendApiService) UpdateLoginFlowExecute(r FrontendApiApiUpdateLoginF return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiUpdateLogoutFlowRequest struct { +type FrontendAPIApiUpdateLogoutFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI token *string returnTo *string cookie *string } -func (r FrontendApiApiUpdateLogoutFlowRequest) Token(token string) FrontendApiApiUpdateLogoutFlowRequest { +func (r FrontendAPIApiUpdateLogoutFlowRequest) Token(token string) FrontendAPIApiUpdateLogoutFlowRequest { r.token = &token return r } -func (r FrontendApiApiUpdateLogoutFlowRequest) ReturnTo(returnTo string) FrontendApiApiUpdateLogoutFlowRequest { +func (r FrontendAPIApiUpdateLogoutFlowRequest) ReturnTo(returnTo string) FrontendAPIApiUpdateLogoutFlowRequest { r.returnTo = &returnTo return r } -func (r FrontendApiApiUpdateLogoutFlowRequest) Cookie(cookie string) FrontendApiApiUpdateLogoutFlowRequest { +func (r FrontendAPIApiUpdateLogoutFlowRequest) Cookie(cookie string) FrontendAPIApiUpdateLogoutFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiUpdateLogoutFlowRequest) Execute() (*http.Response, error) { +func (r FrontendAPIApiUpdateLogoutFlowRequest) Execute() (*http.Response, error) { return r.ApiService.UpdateLogoutFlowExecute(r) } @@ -4962,10 +4970,10 @@ call the `/self-service/logout/api` URL directly with the Ory Session Token. More information can be found at [Ory Kratos User Logout Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-logout). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiUpdateLogoutFlowRequest + - @return FrontendAPIApiUpdateLogoutFlowRequest */ -func (a *FrontendApiService) UpdateLogoutFlow(ctx context.Context) FrontendApiApiUpdateLogoutFlowRequest { - return FrontendApiApiUpdateLogoutFlowRequest{ +func (a *FrontendAPIService) UpdateLogoutFlow(ctx context.Context) FrontendAPIApiUpdateLogoutFlowRequest { + return FrontendAPIApiUpdateLogoutFlowRequest{ ApiService: a, ctx: ctx, } @@ -4974,7 +4982,7 @@ func (a *FrontendApiService) UpdateLogoutFlow(ctx context.Context) FrontendApiAp /* * Execute executes the request */ -func (a *FrontendApiService) UpdateLogoutFlowExecute(r FrontendApiApiUpdateLogoutFlowRequest) (*http.Response, error) { +func (a *FrontendAPIService) UpdateLogoutFlowExecute(r FrontendAPIApiUpdateLogoutFlowRequest) (*http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -4983,7 +4991,7 @@ func (a *FrontendApiService) UpdateLogoutFlowExecute(r FrontendApiApiUpdateLogou localVarFileBytes []byte ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.UpdateLogoutFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.UpdateLogoutFlow") if err != nil { return nil, &GenericOpenAPIError{error: err.Error()} } @@ -5030,7 +5038,7 @@ func (a *FrontendApiService) UpdateLogoutFlowExecute(r FrontendApiApiUpdateLogou return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -5055,33 +5063,33 @@ func (a *FrontendApiService) UpdateLogoutFlowExecute(r FrontendApiApiUpdateLogou return localVarHTTPResponse, nil } -type FrontendApiApiUpdateRecoveryFlowRequest struct { +type FrontendAPIApiUpdateRecoveryFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI flow *string updateRecoveryFlowBody *UpdateRecoveryFlowBody token *string cookie *string } -func (r FrontendApiApiUpdateRecoveryFlowRequest) Flow(flow string) FrontendApiApiUpdateRecoveryFlowRequest { +func (r FrontendAPIApiUpdateRecoveryFlowRequest) Flow(flow string) FrontendAPIApiUpdateRecoveryFlowRequest { r.flow = &flow return r } -func (r FrontendApiApiUpdateRecoveryFlowRequest) UpdateRecoveryFlowBody(updateRecoveryFlowBody UpdateRecoveryFlowBody) FrontendApiApiUpdateRecoveryFlowRequest { +func (r FrontendAPIApiUpdateRecoveryFlowRequest) UpdateRecoveryFlowBody(updateRecoveryFlowBody UpdateRecoveryFlowBody) FrontendAPIApiUpdateRecoveryFlowRequest { r.updateRecoveryFlowBody = &updateRecoveryFlowBody return r } -func (r FrontendApiApiUpdateRecoveryFlowRequest) Token(token string) FrontendApiApiUpdateRecoveryFlowRequest { +func (r FrontendAPIApiUpdateRecoveryFlowRequest) Token(token string) FrontendAPIApiUpdateRecoveryFlowRequest { r.token = &token return r } -func (r FrontendApiApiUpdateRecoveryFlowRequest) Cookie(cookie string) FrontendApiApiUpdateRecoveryFlowRequest { +func (r FrontendAPIApiUpdateRecoveryFlowRequest) Cookie(cookie string) FrontendAPIApiUpdateRecoveryFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiUpdateRecoveryFlowRequest) Execute() (*RecoveryFlow, *http.Response, error) { +func (r FrontendAPIApiUpdateRecoveryFlowRequest) Execute() (*RecoveryFlow, *http.Response, error) { return r.ApiService.UpdateRecoveryFlowExecute(r) } @@ -5105,10 +5113,10 @@ a new Recovery Flow ID which contains an error message that the recovery link wa More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiUpdateRecoveryFlowRequest + - @return FrontendAPIApiUpdateRecoveryFlowRequest */ -func (a *FrontendApiService) UpdateRecoveryFlow(ctx context.Context) FrontendApiApiUpdateRecoveryFlowRequest { - return FrontendApiApiUpdateRecoveryFlowRequest{ +func (a *FrontendAPIService) UpdateRecoveryFlow(ctx context.Context) FrontendAPIApiUpdateRecoveryFlowRequest { + return FrontendAPIApiUpdateRecoveryFlowRequest{ ApiService: a, ctx: ctx, } @@ -5118,7 +5126,7 @@ func (a *FrontendApiService) UpdateRecoveryFlow(ctx context.Context) FrontendApi * Execute executes the request * @return RecoveryFlow */ -func (a *FrontendApiService) UpdateRecoveryFlowExecute(r FrontendApiApiUpdateRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) { +func (a *FrontendAPIService) UpdateRecoveryFlowExecute(r FrontendAPIApiUpdateRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPost localVarPostBody interface{} @@ -5128,7 +5136,7 @@ func (a *FrontendApiService) UpdateRecoveryFlowExecute(r FrontendApiApiUpdateRec localVarReturnValue *RecoveryFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.UpdateRecoveryFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.UpdateRecoveryFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -5181,7 +5189,7 @@ func (a *FrontendApiService) UpdateRecoveryFlowExecute(r FrontendApiApiUpdateRec return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -5245,28 +5253,28 @@ func (a *FrontendApiService) UpdateRecoveryFlowExecute(r FrontendApiApiUpdateRec return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiUpdateRegistrationFlowRequest struct { +type FrontendAPIApiUpdateRegistrationFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI flow *string updateRegistrationFlowBody *UpdateRegistrationFlowBody cookie *string } -func (r FrontendApiApiUpdateRegistrationFlowRequest) Flow(flow string) FrontendApiApiUpdateRegistrationFlowRequest { +func (r FrontendAPIApiUpdateRegistrationFlowRequest) Flow(flow string) FrontendAPIApiUpdateRegistrationFlowRequest { r.flow = &flow return r } -func (r FrontendApiApiUpdateRegistrationFlowRequest) UpdateRegistrationFlowBody(updateRegistrationFlowBody UpdateRegistrationFlowBody) FrontendApiApiUpdateRegistrationFlowRequest { +func (r FrontendAPIApiUpdateRegistrationFlowRequest) UpdateRegistrationFlowBody(updateRegistrationFlowBody UpdateRegistrationFlowBody) FrontendAPIApiUpdateRegistrationFlowRequest { r.updateRegistrationFlowBody = &updateRegistrationFlowBody return r } -func (r FrontendApiApiUpdateRegistrationFlowRequest) Cookie(cookie string) FrontendApiApiUpdateRegistrationFlowRequest { +func (r FrontendAPIApiUpdateRegistrationFlowRequest) Cookie(cookie string) FrontendAPIApiUpdateRegistrationFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiUpdateRegistrationFlowRequest) Execute() (*SuccessfulNativeRegistration, *http.Response, error) { +func (r FrontendAPIApiUpdateRegistrationFlowRequest) Execute() (*SuccessfulNativeRegistration, *http.Response, error) { return r.ApiService.UpdateRegistrationFlowExecute(r) } @@ -5302,10 +5310,10 @@ Most likely used in Social Sign In flows. More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiUpdateRegistrationFlowRequest + - @return FrontendAPIApiUpdateRegistrationFlowRequest */ -func (a *FrontendApiService) UpdateRegistrationFlow(ctx context.Context) FrontendApiApiUpdateRegistrationFlowRequest { - return FrontendApiApiUpdateRegistrationFlowRequest{ +func (a *FrontendAPIService) UpdateRegistrationFlow(ctx context.Context) FrontendAPIApiUpdateRegistrationFlowRequest { + return FrontendAPIApiUpdateRegistrationFlowRequest{ ApiService: a, ctx: ctx, } @@ -5315,7 +5323,7 @@ func (a *FrontendApiService) UpdateRegistrationFlow(ctx context.Context) Fronten * Execute executes the request * @return SuccessfulNativeRegistration */ -func (a *FrontendApiService) UpdateRegistrationFlowExecute(r FrontendApiApiUpdateRegistrationFlowRequest) (*SuccessfulNativeRegistration, *http.Response, error) { +func (a *FrontendAPIService) UpdateRegistrationFlowExecute(r FrontendAPIApiUpdateRegistrationFlowRequest) (*SuccessfulNativeRegistration, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPost localVarPostBody interface{} @@ -5325,7 +5333,7 @@ func (a *FrontendApiService) UpdateRegistrationFlowExecute(r FrontendApiApiUpdat localVarReturnValue *SuccessfulNativeRegistration ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.UpdateRegistrationFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.UpdateRegistrationFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -5375,7 +5383,7 @@ func (a *FrontendApiService) UpdateRegistrationFlowExecute(r FrontendApiApiUpdat return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -5439,33 +5447,33 @@ func (a *FrontendApiService) UpdateRegistrationFlowExecute(r FrontendApiApiUpdat return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiUpdateSettingsFlowRequest struct { +type FrontendAPIApiUpdateSettingsFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI flow *string updateSettingsFlowBody *UpdateSettingsFlowBody xSessionToken *string cookie *string } -func (r FrontendApiApiUpdateSettingsFlowRequest) Flow(flow string) FrontendApiApiUpdateSettingsFlowRequest { +func (r FrontendAPIApiUpdateSettingsFlowRequest) Flow(flow string) FrontendAPIApiUpdateSettingsFlowRequest { r.flow = &flow return r } -func (r FrontendApiApiUpdateSettingsFlowRequest) UpdateSettingsFlowBody(updateSettingsFlowBody UpdateSettingsFlowBody) FrontendApiApiUpdateSettingsFlowRequest { +func (r FrontendAPIApiUpdateSettingsFlowRequest) UpdateSettingsFlowBody(updateSettingsFlowBody UpdateSettingsFlowBody) FrontendAPIApiUpdateSettingsFlowRequest { r.updateSettingsFlowBody = &updateSettingsFlowBody return r } -func (r FrontendApiApiUpdateSettingsFlowRequest) XSessionToken(xSessionToken string) FrontendApiApiUpdateSettingsFlowRequest { +func (r FrontendAPIApiUpdateSettingsFlowRequest) XSessionToken(xSessionToken string) FrontendAPIApiUpdateSettingsFlowRequest { r.xSessionToken = &xSessionToken return r } -func (r FrontendApiApiUpdateSettingsFlowRequest) Cookie(cookie string) FrontendApiApiUpdateSettingsFlowRequest { +func (r FrontendAPIApiUpdateSettingsFlowRequest) Cookie(cookie string) FrontendAPIApiUpdateSettingsFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiUpdateSettingsFlowRequest) Execute() (*SettingsFlow, *http.Response, error) { +func (r FrontendAPIApiUpdateSettingsFlowRequest) Execute() (*SettingsFlow, *http.Response, error) { return r.ApiService.UpdateSettingsFlowExecute(r) } @@ -5516,10 +5524,10 @@ Most likely used in Social Sign In flows. More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiUpdateSettingsFlowRequest + - @return FrontendAPIApiUpdateSettingsFlowRequest */ -func (a *FrontendApiService) UpdateSettingsFlow(ctx context.Context) FrontendApiApiUpdateSettingsFlowRequest { - return FrontendApiApiUpdateSettingsFlowRequest{ +func (a *FrontendAPIService) UpdateSettingsFlow(ctx context.Context) FrontendAPIApiUpdateSettingsFlowRequest { + return FrontendAPIApiUpdateSettingsFlowRequest{ ApiService: a, ctx: ctx, } @@ -5529,7 +5537,7 @@ func (a *FrontendApiService) UpdateSettingsFlow(ctx context.Context) FrontendApi * Execute executes the request * @return SettingsFlow */ -func (a *FrontendApiService) UpdateSettingsFlowExecute(r FrontendApiApiUpdateSettingsFlowRequest) (*SettingsFlow, *http.Response, error) { +func (a *FrontendAPIService) UpdateSettingsFlowExecute(r FrontendAPIApiUpdateSettingsFlowRequest) (*SettingsFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPost localVarPostBody interface{} @@ -5539,7 +5547,7 @@ func (a *FrontendApiService) UpdateSettingsFlowExecute(r FrontendApiApiUpdateSet localVarReturnValue *SettingsFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.UpdateSettingsFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.UpdateSettingsFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -5592,7 +5600,7 @@ func (a *FrontendApiService) UpdateSettingsFlowExecute(r FrontendApiApiUpdateSet return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -5676,33 +5684,33 @@ func (a *FrontendApiService) UpdateSettingsFlowExecute(r FrontendApiApiUpdateSet return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiUpdateVerificationFlowRequest struct { +type FrontendAPIApiUpdateVerificationFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI flow *string updateVerificationFlowBody *UpdateVerificationFlowBody token *string cookie *string } -func (r FrontendApiApiUpdateVerificationFlowRequest) Flow(flow string) FrontendApiApiUpdateVerificationFlowRequest { +func (r FrontendAPIApiUpdateVerificationFlowRequest) Flow(flow string) FrontendAPIApiUpdateVerificationFlowRequest { r.flow = &flow return r } -func (r FrontendApiApiUpdateVerificationFlowRequest) UpdateVerificationFlowBody(updateVerificationFlowBody UpdateVerificationFlowBody) FrontendApiApiUpdateVerificationFlowRequest { +func (r FrontendAPIApiUpdateVerificationFlowRequest) UpdateVerificationFlowBody(updateVerificationFlowBody UpdateVerificationFlowBody) FrontendAPIApiUpdateVerificationFlowRequest { r.updateVerificationFlowBody = &updateVerificationFlowBody return r } -func (r FrontendApiApiUpdateVerificationFlowRequest) Token(token string) FrontendApiApiUpdateVerificationFlowRequest { +func (r FrontendAPIApiUpdateVerificationFlowRequest) Token(token string) FrontendAPIApiUpdateVerificationFlowRequest { r.token = &token return r } -func (r FrontendApiApiUpdateVerificationFlowRequest) Cookie(cookie string) FrontendApiApiUpdateVerificationFlowRequest { +func (r FrontendAPIApiUpdateVerificationFlowRequest) Cookie(cookie string) FrontendAPIApiUpdateVerificationFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiUpdateVerificationFlowRequest) Execute() (*VerificationFlow, *http.Response, error) { +func (r FrontendAPIApiUpdateVerificationFlowRequest) Execute() (*VerificationFlow, *http.Response, error) { return r.ApiService.UpdateVerificationFlowExecute(r) } @@ -5726,10 +5734,10 @@ a new Verification Flow ID which contains an error message that the verification More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiUpdateVerificationFlowRequest + - @return FrontendAPIApiUpdateVerificationFlowRequest */ -func (a *FrontendApiService) UpdateVerificationFlow(ctx context.Context) FrontendApiApiUpdateVerificationFlowRequest { - return FrontendApiApiUpdateVerificationFlowRequest{ +func (a *FrontendAPIService) UpdateVerificationFlow(ctx context.Context) FrontendAPIApiUpdateVerificationFlowRequest { + return FrontendAPIApiUpdateVerificationFlowRequest{ ApiService: a, ctx: ctx, } @@ -5739,7 +5747,7 @@ func (a *FrontendApiService) UpdateVerificationFlow(ctx context.Context) Fronten * Execute executes the request * @return VerificationFlow */ -func (a *FrontendApiService) UpdateVerificationFlowExecute(r FrontendApiApiUpdateVerificationFlowRequest) (*VerificationFlow, *http.Response, error) { +func (a *FrontendAPIService) UpdateVerificationFlowExecute(r FrontendAPIApiUpdateVerificationFlowRequest) (*VerificationFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPost localVarPostBody interface{} @@ -5749,7 +5757,7 @@ func (a *FrontendApiService) UpdateVerificationFlowExecute(r FrontendApiApiUpdat localVarReturnValue *VerificationFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.UpdateVerificationFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.UpdateVerificationFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -5802,7 +5810,7 @@ func (a *FrontendApiService) UpdateVerificationFlowExecute(r FrontendApiApiUpdat return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { diff --git a/internal/client-go/api_identity.go b/internal/client-go/api_identity.go index e19c70b2df40..4a7911095393 100644 --- a/internal/client-go/api_identity.go +++ b/internal/client-go/api_identity.go @@ -26,7 +26,7 @@ var ( _ context.Context ) -type IdentityApi interface { +type IdentityAPI interface { /* * AdminCurrentSessionExtend Calling this endpoint refreshes a current user session. If `session.refresh_min_time_left` is set it will only refresh the session after this time has passed. @@ -95,22 +95,22 @@ type IdentityApi interface { AdminUpdateIdentityBodyExecute(r IdentityApiApiAdminUpdateIdentityBodyRequest) (*Identity, *http.Response, error) /* - * BatchPatchIdentities Create and deletes multiple identities - * Creates or delete multiple + * BatchPatchIdentities Create multiple identities + * Creates multiple [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model). This endpoint can also be used to [import credentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities) for instance passwords, social sign in configurations or multifactor methods. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiBatchPatchIdentitiesRequest + * @return IdentityAPIApiBatchPatchIdentitiesRequest */ - BatchPatchIdentities(ctx context.Context) IdentityApiApiBatchPatchIdentitiesRequest + BatchPatchIdentities(ctx context.Context) IdentityAPIApiBatchPatchIdentitiesRequest /* * BatchPatchIdentitiesExecute executes the request * @return BatchPatchIdentitiesResponse */ - BatchPatchIdentitiesExecute(r IdentityApiApiBatchPatchIdentitiesRequest) (*BatchPatchIdentitiesResponse, *http.Response, error) + BatchPatchIdentitiesExecute(r IdentityAPIApiBatchPatchIdentitiesRequest) (*BatchPatchIdentitiesResponse, *http.Response, error) /* * CreateIdentity Create an Identity @@ -118,45 +118,45 @@ type IdentityApi interface { [import credentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities) for instance passwords, social sign in configurations or multifactor methods. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiCreateIdentityRequest + * @return IdentityAPIApiCreateIdentityRequest */ - CreateIdentity(ctx context.Context) IdentityApiApiCreateIdentityRequest + CreateIdentity(ctx context.Context) IdentityAPIApiCreateIdentityRequest /* * CreateIdentityExecute executes the request * @return Identity */ - CreateIdentityExecute(r IdentityApiApiCreateIdentityRequest) (*Identity, *http.Response, error) + CreateIdentityExecute(r IdentityAPIApiCreateIdentityRequest) (*Identity, *http.Response, error) /* * CreateRecoveryCodeForIdentity Create a Recovery Code * This endpoint creates a recovery code which should be given to the user in order for them to recover (or activate) their account. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiCreateRecoveryCodeForIdentityRequest + * @return IdentityAPIApiCreateRecoveryCodeForIdentityRequest */ - CreateRecoveryCodeForIdentity(ctx context.Context) IdentityApiApiCreateRecoveryCodeForIdentityRequest + CreateRecoveryCodeForIdentity(ctx context.Context) IdentityAPIApiCreateRecoveryCodeForIdentityRequest /* * CreateRecoveryCodeForIdentityExecute executes the request * @return RecoveryCodeForIdentity */ - CreateRecoveryCodeForIdentityExecute(r IdentityApiApiCreateRecoveryCodeForIdentityRequest) (*RecoveryCodeForIdentity, *http.Response, error) + CreateRecoveryCodeForIdentityExecute(r IdentityAPIApiCreateRecoveryCodeForIdentityRequest) (*RecoveryCodeForIdentity, *http.Response, error) /* * CreateRecoveryLinkForIdentity Create a Recovery Link * This endpoint creates a recovery link which should be given to the user in order for them to recover (or activate) their account. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiCreateRecoveryLinkForIdentityRequest + * @return IdentityAPIApiCreateRecoveryLinkForIdentityRequest */ - CreateRecoveryLinkForIdentity(ctx context.Context) IdentityApiApiCreateRecoveryLinkForIdentityRequest + CreateRecoveryLinkForIdentity(ctx context.Context) IdentityAPIApiCreateRecoveryLinkForIdentityRequest /* * CreateRecoveryLinkForIdentityExecute executes the request * @return RecoveryLinkForIdentity */ - CreateRecoveryLinkForIdentityExecute(r IdentityApiApiCreateRecoveryLinkForIdentityRequest) (*RecoveryLinkForIdentity, *http.Response, error) + CreateRecoveryLinkForIdentityExecute(r IdentityAPIApiCreateRecoveryLinkForIdentityRequest) (*RecoveryLinkForIdentity, *http.Response, error) /* * DeleteIdentity Delete an Identity @@ -165,76 +165,83 @@ type IdentityApi interface { assumed that is has been deleted already. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @return IdentityApiApiDeleteIdentityRequest + * @return IdentityAPIApiDeleteIdentityRequest */ - DeleteIdentity(ctx context.Context, id string) IdentityApiApiDeleteIdentityRequest + DeleteIdentity(ctx context.Context, id string) IdentityAPIApiDeleteIdentityRequest /* * DeleteIdentityExecute executes the request */ - DeleteIdentityExecute(r IdentityApiApiDeleteIdentityRequest) (*http.Response, error) + DeleteIdentityExecute(r IdentityAPIApiDeleteIdentityRequest) (*http.Response, error) /* * DeleteIdentityCredentials Delete a credential for a specific identity - * Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type - You can only delete second factor (aal2) credentials. + * Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type. + You cannot delete password or code auth credentials through this API. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode - * @return IdentityApiApiDeleteIdentityCredentialsRequest + * @param type_ Type is the type of credentials to delete. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + * @return IdentityAPIApiDeleteIdentityCredentialsRequest */ - DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest + DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityAPIApiDeleteIdentityCredentialsRequest /* * DeleteIdentityCredentialsExecute executes the request */ - DeleteIdentityCredentialsExecute(r IdentityApiApiDeleteIdentityCredentialsRequest) (*http.Response, error) + DeleteIdentityCredentialsExecute(r IdentityAPIApiDeleteIdentityCredentialsRequest) (*http.Response, error) /* * DeleteIdentitySessions Delete & Invalidate an Identity's Sessions * Calling this endpoint irrecoverably and permanently deletes and invalidates all sessions that belong to the given Identity. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @return IdentityApiApiDeleteIdentitySessionsRequest + * @return IdentityAPIApiDeleteIdentitySessionsRequest */ - DeleteIdentitySessions(ctx context.Context, id string) IdentityApiApiDeleteIdentitySessionsRequest + DeleteIdentitySessions(ctx context.Context, id string) IdentityAPIApiDeleteIdentitySessionsRequest /* * DeleteIdentitySessionsExecute executes the request */ - DeleteIdentitySessionsExecute(r IdentityApiApiDeleteIdentitySessionsRequest) (*http.Response, error) + DeleteIdentitySessionsExecute(r IdentityAPIApiDeleteIdentitySessionsRequest) (*http.Response, error) /* * DisableSession Deactivate a Session * Calling this endpoint deactivates the specified session. Session data is not deleted. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the session's ID. - * @return IdentityApiApiDisableSessionRequest + * @return IdentityAPIApiDisableSessionRequest */ - DisableSession(ctx context.Context, id string) IdentityApiApiDisableSessionRequest + DisableSession(ctx context.Context, id string) IdentityAPIApiDisableSessionRequest /* * DisableSessionExecute executes the request */ - DisableSessionExecute(r IdentityApiApiDisableSessionRequest) (*http.Response, error) + DisableSessionExecute(r IdentityAPIApiDisableSessionRequest) (*http.Response, error) /* * ExtendSession Extend a Session * Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it will only extend the session after the specified time has passed. + This endpoint returns per default a 204 No Content response on success. Older Ory Network projects may + return a 200 OK response with the session in the body. Returning the session as part of the response + will be deprecated in the future and should not be relied upon. + + This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those + scenarios. This endpoint also returns 404 errors if the session does not exist. + Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the session's ID. - * @return IdentityApiApiExtendSessionRequest + * @return IdentityAPIApiExtendSessionRequest */ - ExtendSession(ctx context.Context, id string) IdentityApiApiExtendSessionRequest + ExtendSession(ctx context.Context, id string) IdentityAPIApiExtendSessionRequest /* * ExtendSessionExecute executes the request * @return Session */ - ExtendSessionExecute(r IdentityApiApiExtendSessionRequest) (*Session, *http.Response, error) + ExtendSessionExecute(r IdentityAPIApiExtendSessionRequest) (*Session, *http.Response, error) /* * GetIdentity Get an Identity @@ -242,30 +249,30 @@ type IdentityApi interface { include credentials (e.g. social sign in connections) in the response by using the `include_credential` query parameter. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID must be set to the ID of identity you want to get - * @return IdentityApiApiGetIdentityRequest + * @return IdentityAPIApiGetIdentityRequest */ - GetIdentity(ctx context.Context, id string) IdentityApiApiGetIdentityRequest + GetIdentity(ctx context.Context, id string) IdentityAPIApiGetIdentityRequest /* * GetIdentityExecute executes the request * @return Identity */ - GetIdentityExecute(r IdentityApiApiGetIdentityRequest) (*Identity, *http.Response, error) + GetIdentityExecute(r IdentityAPIApiGetIdentityRequest) (*Identity, *http.Response, error) /* * GetIdentitySchema Get Identity JSON Schema * Return a specific identity schema. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID must be set to the ID of schema you want to get - * @return IdentityApiApiGetIdentitySchemaRequest + * @return IdentityAPIApiGetIdentitySchemaRequest */ - GetIdentitySchema(ctx context.Context, id string) IdentityApiApiGetIdentitySchemaRequest + GetIdentitySchema(ctx context.Context, id string) IdentityAPIApiGetIdentitySchemaRequest /* * GetIdentitySchemaExecute executes the request * @return map[string]interface{} */ - GetIdentitySchemaExecute(r IdentityApiApiGetIdentitySchemaRequest) (map[string]interface{}, *http.Response, error) + GetIdentitySchemaExecute(r IdentityAPIApiGetIdentitySchemaRequest) (map[string]interface{}, *http.Response, error) /* * GetSession Get Session @@ -274,72 +281,72 @@ type IdentityApi interface { Getting a session object with all specified expandables that exist in an administrative context. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the session's ID. - * @return IdentityApiApiGetSessionRequest + * @return IdentityAPIApiGetSessionRequest */ - GetSession(ctx context.Context, id string) IdentityApiApiGetSessionRequest + GetSession(ctx context.Context, id string) IdentityAPIApiGetSessionRequest /* * GetSessionExecute executes the request * @return Session */ - GetSessionExecute(r IdentityApiApiGetSessionRequest) (*Session, *http.Response, error) + GetSessionExecute(r IdentityAPIApiGetSessionRequest) (*Session, *http.Response, error) /* * ListIdentities List Identities * Lists all [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model) in the system. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiListIdentitiesRequest + * @return IdentityAPIApiListIdentitiesRequest */ - ListIdentities(ctx context.Context) IdentityApiApiListIdentitiesRequest + ListIdentities(ctx context.Context) IdentityAPIApiListIdentitiesRequest /* * ListIdentitiesExecute executes the request * @return []Identity */ - ListIdentitiesExecute(r IdentityApiApiListIdentitiesRequest) ([]Identity, *http.Response, error) + ListIdentitiesExecute(r IdentityAPIApiListIdentitiesRequest) ([]Identity, *http.Response, error) /* * ListIdentitySchemas Get all Identity Schemas * Returns a list of all identity schemas currently in use. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiListIdentitySchemasRequest + * @return IdentityAPIApiListIdentitySchemasRequest */ - ListIdentitySchemas(ctx context.Context) IdentityApiApiListIdentitySchemasRequest + ListIdentitySchemas(ctx context.Context) IdentityAPIApiListIdentitySchemasRequest /* * ListIdentitySchemasExecute executes the request * @return []IdentitySchemaContainer */ - ListIdentitySchemasExecute(r IdentityApiApiListIdentitySchemasRequest) ([]IdentitySchemaContainer, *http.Response, error) + ListIdentitySchemasExecute(r IdentityAPIApiListIdentitySchemasRequest) ([]IdentitySchemaContainer, *http.Response, error) /* * ListIdentitySessions List an Identity's Sessions * This endpoint returns all sessions that belong to the given Identity. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @return IdentityApiApiListIdentitySessionsRequest + * @return IdentityAPIApiListIdentitySessionsRequest */ - ListIdentitySessions(ctx context.Context, id string) IdentityApiApiListIdentitySessionsRequest + ListIdentitySessions(ctx context.Context, id string) IdentityAPIApiListIdentitySessionsRequest /* * ListIdentitySessionsExecute executes the request * @return []Session */ - ListIdentitySessionsExecute(r IdentityApiApiListIdentitySessionsRequest) ([]Session, *http.Response, error) + ListIdentitySessionsExecute(r IdentityAPIApiListIdentitySessionsRequest) ([]Session, *http.Response, error) /* * ListSessions List All Sessions * Listing all sessions that exist. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiListSessionsRequest + * @return IdentityAPIApiListSessionsRequest */ - ListSessions(ctx context.Context) IdentityApiApiListSessionsRequest + ListSessions(ctx context.Context) IdentityAPIApiListSessionsRequest /* * ListSessionsExecute executes the request * @return []Session */ - ListSessionsExecute(r IdentityApiApiListSessionsRequest) ([]Session, *http.Response, error) + ListSessionsExecute(r IdentityAPIApiListSessionsRequest) ([]Session, *http.Response, error) /* * PatchIdentity Patch an Identity @@ -347,15 +354,15 @@ type IdentityApi interface { The fields `id`, `stateChangedAt` and `credentials` can not be updated using this method. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID must be set to the ID of identity you want to update - * @return IdentityApiApiPatchIdentityRequest + * @return IdentityAPIApiPatchIdentityRequest */ - PatchIdentity(ctx context.Context, id string) IdentityApiApiPatchIdentityRequest + PatchIdentity(ctx context.Context, id string) IdentityAPIApiPatchIdentityRequest /* * PatchIdentityExecute executes the request * @return Identity */ - PatchIdentityExecute(r IdentityApiApiPatchIdentityRequest) (*Identity, *http.Response, error) + PatchIdentityExecute(r IdentityAPIApiPatchIdentityRequest) (*Identity, *http.Response, error) /* * UpdateIdentity Update an Identity @@ -363,23 +370,23 @@ type IdentityApi interface { payload (except credentials) is expected. It is possible to update the identity's credentials as well. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID must be set to the ID of identity you want to update - * @return IdentityApiApiUpdateIdentityRequest + * @return IdentityAPIApiUpdateIdentityRequest */ - UpdateIdentity(ctx context.Context, id string) IdentityApiApiUpdateIdentityRequest + UpdateIdentity(ctx context.Context, id string) IdentityAPIApiUpdateIdentityRequest /* * UpdateIdentityExecute executes the request * @return Identity */ - UpdateIdentityExecute(r IdentityApiApiUpdateIdentityRequest) (*Identity, *http.Response, error) + UpdateIdentityExecute(r IdentityAPIApiUpdateIdentityRequest) (*Identity, *http.Response, error) } -// IdentityApiService IdentityApi service -type IdentityApiService service +// IdentityAPIService IdentityAPI service +type IdentityAPIService service type IdentityApiApiAdminCurrentSessionExtendRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI } func (r IdentityApiApiAdminCurrentSessionExtendRequest) Execute() (*Session, *http.Response, error) { @@ -392,9 +399,9 @@ func (r IdentityApiApiAdminCurrentSessionExtendRequest) Execute() (*Session, *ht Session refresh - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return IdentityApiApiAdminCurrentSessionExtendRequest + - @return IdentityAPIApiAdminCurrentSessionExtendRequest */ -func (a *IdentityApiService) AdminCurrentSessionExtend(ctx context.Context) IdentityApiApiAdminCurrentSessionExtendRequest { +func (a *IdentityAPIService) AdminCurrentSessionExtend(ctx context.Context) IdentityApiApiAdminCurrentSessionExtendRequest { return IdentityApiApiAdminCurrentSessionExtendRequest{ ApiService: a, ctx: ctx, @@ -405,7 +412,7 @@ func (a *IdentityApiService) AdminCurrentSessionExtend(ctx context.Context) Iden * Execute executes the request * @return Session */ -func (a *IdentityApiService) AdminCurrentSessionExtendExecute(r IdentityApiApiAdminCurrentSessionExtendRequest) (*Session, *http.Response, error) { +func (a *IdentityAPIService) AdminCurrentSessionExtendExecute(r IdentityApiApiAdminCurrentSessionExtendRequest) (*Session, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -515,7 +522,7 @@ func (a *IdentityApiService) AdminCurrentSessionExtendExecute(r IdentityApiApiAd type IdentityApiApiAdminIdentitySessionRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string upgrade *bool } @@ -538,7 +545,7 @@ Issuing session or session token for a given identity without authenticating - @param id ID is the identity's ID. - @return IdentityApiApiAdminIdentitySessionRequest */ -func (a *IdentityApiService) AdminIdentitySession(ctx context.Context, id string) IdentityApiApiAdminIdentitySessionRequest { +func (a *IdentityAPIService) AdminIdentitySession(ctx context.Context, id string) IdentityApiApiAdminIdentitySessionRequest { return IdentityApiApiAdminIdentitySessionRequest{ ApiService: a, ctx: ctx, @@ -550,7 +557,7 @@ func (a *IdentityApiService) AdminIdentitySession(ctx context.Context, id string * Execute executes the request * @return SuccessfulAdminIdentitySession */ -func (a *IdentityApiService) AdminIdentitySessionExecute(r IdentityApiApiAdminIdentitySessionRequest) (*SuccessfulAdminIdentitySession, *http.Response, error) { +func (a *IdentityAPIService) AdminIdentitySessionExecute(r IdentityApiApiAdminIdentitySessionRequest) (*SuccessfulAdminIdentitySession, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -664,7 +671,7 @@ func (a *IdentityApiService) AdminIdentitySessionExecute(r IdentityApiApiAdminId type IdentityApiApiAdminUpdateCredentialsRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string } @@ -681,7 +688,7 @@ Learn how identities work in [Ory Kratos' User And Identity Model Documentation] - @param id ID is the identity's ID. - @return IdentityApiApiAdminUpdateCredentialsRequest */ -func (a *IdentityApiService) AdminUpdateCredentials(ctx context.Context, id string) IdentityApiApiAdminUpdateCredentialsRequest { +func (a *IdentityAPIService) AdminUpdateCredentials(ctx context.Context, id string) IdentityApiApiAdminUpdateCredentialsRequest { return IdentityApiApiAdminUpdateCredentialsRequest{ ApiService: a, ctx: ctx, @@ -692,7 +699,7 @@ func (a *IdentityApiService) AdminUpdateCredentials(ctx context.Context, id stri /* * Execute executes the request */ -func (a *IdentityApiService) AdminUpdateCredentialsExecute(r IdentityApiApiAdminUpdateCredentialsRequest) (*http.Response, error) { +func (a *IdentityAPIService) AdminUpdateCredentialsExecute(r IdentityApiApiAdminUpdateCredentialsRequest) (*http.Response, error) { var ( localVarHTTPMethod = http.MethodPut localVarPostBody interface{} @@ -779,7 +786,7 @@ func (a *IdentityApiService) AdminUpdateCredentialsExecute(r IdentityApiApiAdmin type IdentityApiApiAdminUpdateIdentityBodyRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI } func (r IdentityApiApiAdminUpdateIdentityBodyRequest) Execute() (*Identity, *http.Response, error) { @@ -796,7 +803,7 @@ Learn how identities work in [Ory Kratos' User And Identity Model Documentation] - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @return IdentityApiApiAdminUpdateIdentityBodyRequest */ -func (a *IdentityApiService) AdminUpdateIdentityBody(ctx context.Context) IdentityApiApiAdminUpdateIdentityBodyRequest { +func (a *IdentityAPIService) AdminUpdateIdentityBody(ctx context.Context) IdentityApiApiAdminUpdateIdentityBodyRequest { return IdentityApiApiAdminUpdateIdentityBodyRequest{ ApiService: a, ctx: ctx, @@ -807,7 +814,7 @@ func (a *IdentityApiService) AdminUpdateIdentityBody(ctx context.Context) Identi * Execute executes the request * @return Identity */ -func (a *IdentityApiService) AdminUpdateIdentityBodyExecute(r IdentityApiApiAdminUpdateIdentityBodyRequest) (*Identity, *http.Response, error) { +func (a *IdentityAPIService) AdminUpdateIdentityBodyExecute(r IdentityApiApiAdminUpdateIdentityBodyRequest) (*Identity, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPost localVarPostBody interface{} @@ -903,32 +910,38 @@ func (a *IdentityApiService) AdminUpdateIdentityBodyExecute(r IdentityApiApiAdmi type IdentityApiApiBatchPatchIdentitiesRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI + patchIdentitiesBody *PatchIdentitiesBody +} + +type IdentityAPIApiBatchPatchIdentitiesRequest struct { + ctx context.Context + ApiService IdentityAPI patchIdentitiesBody *PatchIdentitiesBody } -func (r IdentityApiApiBatchPatchIdentitiesRequest) PatchIdentitiesBody(patchIdentitiesBody PatchIdentitiesBody) IdentityApiApiBatchPatchIdentitiesRequest { +func (r IdentityAPIApiBatchPatchIdentitiesRequest) PatchIdentitiesBody(patchIdentitiesBody PatchIdentitiesBody) IdentityAPIApiBatchPatchIdentitiesRequest { r.patchIdentitiesBody = &patchIdentitiesBody return r } -func (r IdentityApiApiBatchPatchIdentitiesRequest) Execute() (*BatchPatchIdentitiesResponse, *http.Response, error) { +func (r IdentityAPIApiBatchPatchIdentitiesRequest) Execute() (*BatchPatchIdentitiesResponse, *http.Response, error) { return r.ApiService.BatchPatchIdentitiesExecute(r) } /* - - BatchPatchIdentities Create and deletes multiple identities - - Creates or delete multiple + - BatchPatchIdentities Create multiple identities + - Creates multiple [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model). This endpoint can also be used to [import credentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities) for instance passwords, social sign in configurations or multifactor methods. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return IdentityApiApiBatchPatchIdentitiesRequest + - @return IdentityAPIApiBatchPatchIdentitiesRequest */ -func (a *IdentityApiService) BatchPatchIdentities(ctx context.Context) IdentityApiApiBatchPatchIdentitiesRequest { - return IdentityApiApiBatchPatchIdentitiesRequest{ +func (a *IdentityAPIService) BatchPatchIdentities(ctx context.Context) IdentityAPIApiBatchPatchIdentitiesRequest { + return IdentityAPIApiBatchPatchIdentitiesRequest{ ApiService: a, ctx: ctx, } @@ -938,7 +951,7 @@ func (a *IdentityApiService) BatchPatchIdentities(ctx context.Context) IdentityA * Execute executes the request * @return BatchPatchIdentitiesResponse */ -func (a *IdentityApiService) BatchPatchIdentitiesExecute(r IdentityApiApiBatchPatchIdentitiesRequest) (*BatchPatchIdentitiesResponse, *http.Response, error) { +func (a *IdentityAPIService) BatchPatchIdentitiesExecute(r IdentityAPIApiBatchPatchIdentitiesRequest) (*BatchPatchIdentitiesResponse, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPatch localVarPostBody interface{} @@ -948,7 +961,7 @@ func (a *IdentityApiService) BatchPatchIdentitiesExecute(r IdentityApiApiBatchPa localVarReturnValue *BatchPatchIdentitiesResponse ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.BatchPatchIdentities") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.BatchPatchIdentities") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1002,7 +1015,7 @@ func (a *IdentityApiService) BatchPatchIdentitiesExecute(r IdentityApiApiBatchPa return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1056,18 +1069,18 @@ func (a *IdentityApiService) BatchPatchIdentitiesExecute(r IdentityApiApiBatchPa return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiCreateIdentityRequest struct { +type IdentityAPIApiCreateIdentityRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI createIdentityBody *CreateIdentityBody } -func (r IdentityApiApiCreateIdentityRequest) CreateIdentityBody(createIdentityBody CreateIdentityBody) IdentityApiApiCreateIdentityRequest { +func (r IdentityAPIApiCreateIdentityRequest) CreateIdentityBody(createIdentityBody CreateIdentityBody) IdentityAPIApiCreateIdentityRequest { r.createIdentityBody = &createIdentityBody return r } -func (r IdentityApiApiCreateIdentityRequest) Execute() (*Identity, *http.Response, error) { +func (r IdentityAPIApiCreateIdentityRequest) Execute() (*Identity, *http.Response, error) { return r.ApiService.CreateIdentityExecute(r) } @@ -1078,10 +1091,10 @@ func (r IdentityApiApiCreateIdentityRequest) Execute() (*Identity, *http.Respons [import credentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities) for instance passwords, social sign in configurations or multifactor methods. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return IdentityApiApiCreateIdentityRequest + - @return IdentityAPIApiCreateIdentityRequest */ -func (a *IdentityApiService) CreateIdentity(ctx context.Context) IdentityApiApiCreateIdentityRequest { - return IdentityApiApiCreateIdentityRequest{ +func (a *IdentityAPIService) CreateIdentity(ctx context.Context) IdentityAPIApiCreateIdentityRequest { + return IdentityAPIApiCreateIdentityRequest{ ApiService: a, ctx: ctx, } @@ -1091,7 +1104,7 @@ func (a *IdentityApiService) CreateIdentity(ctx context.Context) IdentityApiApiC * Execute executes the request * @return Identity */ -func (a *IdentityApiService) CreateIdentityExecute(r IdentityApiApiCreateIdentityRequest) (*Identity, *http.Response, error) { +func (a *IdentityAPIService) CreateIdentityExecute(r IdentityAPIApiCreateIdentityRequest) (*Identity, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPost localVarPostBody interface{} @@ -1101,7 +1114,7 @@ func (a *IdentityApiService) CreateIdentityExecute(r IdentityApiApiCreateIdentit localVarReturnValue *Identity ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.CreateIdentity") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.CreateIdentity") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1155,7 +1168,7 @@ func (a *IdentityApiService) CreateIdentityExecute(r IdentityApiApiCreateIdentit return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1209,18 +1222,18 @@ func (a *IdentityApiService) CreateIdentityExecute(r IdentityApiApiCreateIdentit return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiCreateRecoveryCodeForIdentityRequest struct { +type IdentityAPIApiCreateRecoveryCodeForIdentityRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI createRecoveryCodeForIdentityBody *CreateRecoveryCodeForIdentityBody } -func (r IdentityApiApiCreateRecoveryCodeForIdentityRequest) CreateRecoveryCodeForIdentityBody(createRecoveryCodeForIdentityBody CreateRecoveryCodeForIdentityBody) IdentityApiApiCreateRecoveryCodeForIdentityRequest { +func (r IdentityAPIApiCreateRecoveryCodeForIdentityRequest) CreateRecoveryCodeForIdentityBody(createRecoveryCodeForIdentityBody CreateRecoveryCodeForIdentityBody) IdentityAPIApiCreateRecoveryCodeForIdentityRequest { r.createRecoveryCodeForIdentityBody = &createRecoveryCodeForIdentityBody return r } -func (r IdentityApiApiCreateRecoveryCodeForIdentityRequest) Execute() (*RecoveryCodeForIdentity, *http.Response, error) { +func (r IdentityAPIApiCreateRecoveryCodeForIdentityRequest) Execute() (*RecoveryCodeForIdentity, *http.Response, error) { return r.ApiService.CreateRecoveryCodeForIdentityExecute(r) } @@ -1230,10 +1243,10 @@ func (r IdentityApiApiCreateRecoveryCodeForIdentityRequest) Execute() (*Recovery (or activate) their account. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return IdentityApiApiCreateRecoveryCodeForIdentityRequest + - @return IdentityAPIApiCreateRecoveryCodeForIdentityRequest */ -func (a *IdentityApiService) CreateRecoveryCodeForIdentity(ctx context.Context) IdentityApiApiCreateRecoveryCodeForIdentityRequest { - return IdentityApiApiCreateRecoveryCodeForIdentityRequest{ +func (a *IdentityAPIService) CreateRecoveryCodeForIdentity(ctx context.Context) IdentityAPIApiCreateRecoveryCodeForIdentityRequest { + return IdentityAPIApiCreateRecoveryCodeForIdentityRequest{ ApiService: a, ctx: ctx, } @@ -1243,7 +1256,7 @@ func (a *IdentityApiService) CreateRecoveryCodeForIdentity(ctx context.Context) * Execute executes the request * @return RecoveryCodeForIdentity */ -func (a *IdentityApiService) CreateRecoveryCodeForIdentityExecute(r IdentityApiApiCreateRecoveryCodeForIdentityRequest) (*RecoveryCodeForIdentity, *http.Response, error) { +func (a *IdentityAPIService) CreateRecoveryCodeForIdentityExecute(r IdentityAPIApiCreateRecoveryCodeForIdentityRequest) (*RecoveryCodeForIdentity, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPost localVarPostBody interface{} @@ -1253,7 +1266,7 @@ func (a *IdentityApiService) CreateRecoveryCodeForIdentityExecute(r IdentityApiA localVarReturnValue *RecoveryCodeForIdentity ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.CreateRecoveryCodeForIdentity") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.CreateRecoveryCodeForIdentity") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1307,7 +1320,7 @@ func (a *IdentityApiService) CreateRecoveryCodeForIdentityExecute(r IdentityApiA return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1361,23 +1374,23 @@ func (a *IdentityApiService) CreateRecoveryCodeForIdentityExecute(r IdentityApiA return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiCreateRecoveryLinkForIdentityRequest struct { +type IdentityAPIApiCreateRecoveryLinkForIdentityRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI returnTo *string createRecoveryLinkForIdentityBody *CreateRecoveryLinkForIdentityBody } -func (r IdentityApiApiCreateRecoveryLinkForIdentityRequest) ReturnTo(returnTo string) IdentityApiApiCreateRecoveryLinkForIdentityRequest { +func (r IdentityAPIApiCreateRecoveryLinkForIdentityRequest) ReturnTo(returnTo string) IdentityAPIApiCreateRecoveryLinkForIdentityRequest { r.returnTo = &returnTo return r } -func (r IdentityApiApiCreateRecoveryLinkForIdentityRequest) CreateRecoveryLinkForIdentityBody(createRecoveryLinkForIdentityBody CreateRecoveryLinkForIdentityBody) IdentityApiApiCreateRecoveryLinkForIdentityRequest { +func (r IdentityAPIApiCreateRecoveryLinkForIdentityRequest) CreateRecoveryLinkForIdentityBody(createRecoveryLinkForIdentityBody CreateRecoveryLinkForIdentityBody) IdentityAPIApiCreateRecoveryLinkForIdentityRequest { r.createRecoveryLinkForIdentityBody = &createRecoveryLinkForIdentityBody return r } -func (r IdentityApiApiCreateRecoveryLinkForIdentityRequest) Execute() (*RecoveryLinkForIdentity, *http.Response, error) { +func (r IdentityAPIApiCreateRecoveryLinkForIdentityRequest) Execute() (*RecoveryLinkForIdentity, *http.Response, error) { return r.ApiService.CreateRecoveryLinkForIdentityExecute(r) } @@ -1387,10 +1400,10 @@ func (r IdentityApiApiCreateRecoveryLinkForIdentityRequest) Execute() (*Recovery (or activate) their account. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return IdentityApiApiCreateRecoveryLinkForIdentityRequest + - @return IdentityAPIApiCreateRecoveryLinkForIdentityRequest */ -func (a *IdentityApiService) CreateRecoveryLinkForIdentity(ctx context.Context) IdentityApiApiCreateRecoveryLinkForIdentityRequest { - return IdentityApiApiCreateRecoveryLinkForIdentityRequest{ +func (a *IdentityAPIService) CreateRecoveryLinkForIdentity(ctx context.Context) IdentityAPIApiCreateRecoveryLinkForIdentityRequest { + return IdentityAPIApiCreateRecoveryLinkForIdentityRequest{ ApiService: a, ctx: ctx, } @@ -1400,7 +1413,7 @@ func (a *IdentityApiService) CreateRecoveryLinkForIdentity(ctx context.Context) * Execute executes the request * @return RecoveryLinkForIdentity */ -func (a *IdentityApiService) CreateRecoveryLinkForIdentityExecute(r IdentityApiApiCreateRecoveryLinkForIdentityRequest) (*RecoveryLinkForIdentity, *http.Response, error) { +func (a *IdentityAPIService) CreateRecoveryLinkForIdentityExecute(r IdentityAPIApiCreateRecoveryLinkForIdentityRequest) (*RecoveryLinkForIdentity, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPost localVarPostBody interface{} @@ -1410,7 +1423,7 @@ func (a *IdentityApiService) CreateRecoveryLinkForIdentityExecute(r IdentityApiA localVarReturnValue *RecoveryLinkForIdentity ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.CreateRecoveryLinkForIdentity") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.CreateRecoveryLinkForIdentity") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1467,7 +1480,7 @@ func (a *IdentityApiService) CreateRecoveryLinkForIdentityExecute(r IdentityApiA return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1521,13 +1534,13 @@ func (a *IdentityApiService) CreateRecoveryLinkForIdentityExecute(r IdentityApiA return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiDeleteIdentityRequest struct { +type IdentityAPIApiDeleteIdentityRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string } -func (r IdentityApiApiDeleteIdentityRequest) Execute() (*http.Response, error) { +func (r IdentityAPIApiDeleteIdentityRequest) Execute() (*http.Response, error) { return r.ApiService.DeleteIdentityExecute(r) } @@ -1539,10 +1552,10 @@ This endpoint returns 204 when the identity was deleted or when the identity was assumed that is has been deleted already. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the identity's ID. - - @return IdentityApiApiDeleteIdentityRequest + - @return IdentityAPIApiDeleteIdentityRequest */ -func (a *IdentityApiService) DeleteIdentity(ctx context.Context, id string) IdentityApiApiDeleteIdentityRequest { - return IdentityApiApiDeleteIdentityRequest{ +func (a *IdentityAPIService) DeleteIdentity(ctx context.Context, id string) IdentityAPIApiDeleteIdentityRequest { + return IdentityAPIApiDeleteIdentityRequest{ ApiService: a, ctx: ctx, id: id, @@ -1552,7 +1565,7 @@ func (a *IdentityApiService) DeleteIdentity(ctx context.Context, id string) Iden /* * Execute executes the request */ -func (a *IdentityApiService) DeleteIdentityExecute(r IdentityApiApiDeleteIdentityRequest) (*http.Response, error) { +func (a *IdentityAPIService) DeleteIdentityExecute(r IdentityAPIApiDeleteIdentityRequest) (*http.Response, error) { var ( localVarHTTPMethod = http.MethodDelete localVarPostBody interface{} @@ -1561,7 +1574,7 @@ func (a *IdentityApiService) DeleteIdentityExecute(r IdentityApiApiDeleteIdentit localVarFileBytes []byte ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.DeleteIdentity") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.DeleteIdentity") if err != nil { return nil, &GenericOpenAPIError{error: err.Error()} } @@ -1614,7 +1627,7 @@ func (a *IdentityApiService) DeleteIdentityExecute(r IdentityApiApiDeleteIdentit return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1649,29 +1662,35 @@ func (a *IdentityApiService) DeleteIdentityExecute(r IdentityApiApiDeleteIdentit return localVarHTTPResponse, nil } -type IdentityApiApiDeleteIdentityCredentialsRequest struct { +type IdentityAPIApiDeleteIdentityCredentialsRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string type_ string + identifier *string } -func (r IdentityApiApiDeleteIdentityCredentialsRequest) Execute() (*http.Response, error) { +func (r IdentityAPIApiDeleteIdentityCredentialsRequest) Identifier(identifier string) IdentityAPIApiDeleteIdentityCredentialsRequest { + r.identifier = &identifier + return r +} + +func (r IdentityAPIApiDeleteIdentityCredentialsRequest) Execute() (*http.Response, error) { return r.ApiService.DeleteIdentityCredentialsExecute(r) } /* - DeleteIdentityCredentials Delete a credential for a specific identity - - Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type + - Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type. -You can only delete second factor (aal2) credentials. +You cannot delete password or code auth credentials through this API. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the identity's ID. - - @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode - - @return IdentityApiApiDeleteIdentityCredentialsRequest + - @param type_ Type is the type of credentials to delete. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + - @return IdentityAPIApiDeleteIdentityCredentialsRequest */ -func (a *IdentityApiService) DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest { - return IdentityApiApiDeleteIdentityCredentialsRequest{ +func (a *IdentityAPIService) DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityAPIApiDeleteIdentityCredentialsRequest { + return IdentityAPIApiDeleteIdentityCredentialsRequest{ ApiService: a, ctx: ctx, id: id, @@ -1682,7 +1701,7 @@ func (a *IdentityApiService) DeleteIdentityCredentials(ctx context.Context, id s /* * Execute executes the request */ -func (a *IdentityApiService) DeleteIdentityCredentialsExecute(r IdentityApiApiDeleteIdentityCredentialsRequest) (*http.Response, error) { +func (a *IdentityAPIService) DeleteIdentityCredentialsExecute(r IdentityAPIApiDeleteIdentityCredentialsRequest) (*http.Response, error) { var ( localVarHTTPMethod = http.MethodDelete localVarPostBody interface{} @@ -1691,7 +1710,7 @@ func (a *IdentityApiService) DeleteIdentityCredentialsExecute(r IdentityApiApiDe localVarFileBytes []byte ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.DeleteIdentityCredentials") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.DeleteIdentityCredentials") if err != nil { return nil, &GenericOpenAPIError{error: err.Error()} } @@ -1704,6 +1723,9 @@ func (a *IdentityApiService) DeleteIdentityCredentialsExecute(r IdentityApiApiDe localVarQueryParams := url.Values{} localVarFormParams := url.Values{} + if r.identifier != nil { + localVarQueryParams.Add("identifier", parameterToString(*r.identifier, "")) + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} @@ -1745,7 +1767,7 @@ func (a *IdentityApiService) DeleteIdentityCredentialsExecute(r IdentityApiApiDe return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1780,13 +1802,13 @@ func (a *IdentityApiService) DeleteIdentityCredentialsExecute(r IdentityApiApiDe return localVarHTTPResponse, nil } -type IdentityApiApiDeleteIdentitySessionsRequest struct { +type IdentityAPIApiDeleteIdentitySessionsRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string } -func (r IdentityApiApiDeleteIdentitySessionsRequest) Execute() (*http.Response, error) { +func (r IdentityAPIApiDeleteIdentitySessionsRequest) Execute() (*http.Response, error) { return r.ApiService.DeleteIdentitySessionsExecute(r) } @@ -1795,10 +1817,10 @@ func (r IdentityApiApiDeleteIdentitySessionsRequest) Execute() (*http.Response, * Calling this endpoint irrecoverably and permanently deletes and invalidates all sessions that belong to the given Identity. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @return IdentityApiApiDeleteIdentitySessionsRequest + * @return IdentityAPIApiDeleteIdentitySessionsRequest */ -func (a *IdentityApiService) DeleteIdentitySessions(ctx context.Context, id string) IdentityApiApiDeleteIdentitySessionsRequest { - return IdentityApiApiDeleteIdentitySessionsRequest{ +func (a *IdentityAPIService) DeleteIdentitySessions(ctx context.Context, id string) IdentityAPIApiDeleteIdentitySessionsRequest { + return IdentityAPIApiDeleteIdentitySessionsRequest{ ApiService: a, ctx: ctx, id: id, @@ -1808,7 +1830,7 @@ func (a *IdentityApiService) DeleteIdentitySessions(ctx context.Context, id stri /* * Execute executes the request */ -func (a *IdentityApiService) DeleteIdentitySessionsExecute(r IdentityApiApiDeleteIdentitySessionsRequest) (*http.Response, error) { +func (a *IdentityAPIService) DeleteIdentitySessionsExecute(r IdentityAPIApiDeleteIdentitySessionsRequest) (*http.Response, error) { var ( localVarHTTPMethod = http.MethodDelete localVarPostBody interface{} @@ -1817,7 +1839,7 @@ func (a *IdentityApiService) DeleteIdentitySessionsExecute(r IdentityApiApiDelet localVarFileBytes []byte ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.DeleteIdentitySessions") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.DeleteIdentitySessions") if err != nil { return nil, &GenericOpenAPIError{error: err.Error()} } @@ -1870,7 +1892,7 @@ func (a *IdentityApiService) DeleteIdentitySessionsExecute(r IdentityApiApiDelet return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1925,13 +1947,13 @@ func (a *IdentityApiService) DeleteIdentitySessionsExecute(r IdentityApiApiDelet return localVarHTTPResponse, nil } -type IdentityApiApiDisableSessionRequest struct { +type IdentityAPIApiDisableSessionRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string } -func (r IdentityApiApiDisableSessionRequest) Execute() (*http.Response, error) { +func (r IdentityAPIApiDisableSessionRequest) Execute() (*http.Response, error) { return r.ApiService.DisableSessionExecute(r) } @@ -1940,10 +1962,10 @@ func (r IdentityApiApiDisableSessionRequest) Execute() (*http.Response, error) { * Calling this endpoint deactivates the specified session. Session data is not deleted. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the session's ID. - * @return IdentityApiApiDisableSessionRequest + * @return IdentityAPIApiDisableSessionRequest */ -func (a *IdentityApiService) DisableSession(ctx context.Context, id string) IdentityApiApiDisableSessionRequest { - return IdentityApiApiDisableSessionRequest{ +func (a *IdentityAPIService) DisableSession(ctx context.Context, id string) IdentityAPIApiDisableSessionRequest { + return IdentityAPIApiDisableSessionRequest{ ApiService: a, ctx: ctx, id: id, @@ -1953,7 +1975,7 @@ func (a *IdentityApiService) DisableSession(ctx context.Context, id string) Iden /* * Execute executes the request */ -func (a *IdentityApiService) DisableSessionExecute(r IdentityApiApiDisableSessionRequest) (*http.Response, error) { +func (a *IdentityAPIService) DisableSessionExecute(r IdentityAPIApiDisableSessionRequest) (*http.Response, error) { var ( localVarHTTPMethod = http.MethodDelete localVarPostBody interface{} @@ -1962,7 +1984,7 @@ func (a *IdentityApiService) DisableSessionExecute(r IdentityApiApiDisableSessio localVarFileBytes []byte ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.DisableSession") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.DisableSession") if err != nil { return nil, &GenericOpenAPIError{error: err.Error()} } @@ -2015,7 +2037,7 @@ func (a *IdentityApiService) DisableSessionExecute(r IdentityApiApiDisableSessio return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2060,13 +2082,13 @@ func (a *IdentityApiService) DisableSessionExecute(r IdentityApiApiDisableSessio return localVarHTTPResponse, nil } -type IdentityApiApiExtendSessionRequest struct { +type IdentityAPIApiExtendSessionRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string } -func (r IdentityApiApiExtendSessionRequest) Execute() (*Session, *http.Response, error) { +func (r IdentityAPIApiExtendSessionRequest) Execute() (*Session, *http.Response, error) { return r.ApiService.ExtendSessionExecute(r) } @@ -2076,13 +2098,20 @@ func (r IdentityApiApiExtendSessionRequest) Execute() (*Session, *http.Response, will only extend the session after the specified time has passed. +This endpoint returns per default a 204 No Content response on success. Older Ory Network projects may +return a 200 OK response with the session in the body. Returning the session as part of the response +will be deprecated in the future and should not be relied upon. + +This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those +scenarios. This endpoint also returns 404 errors if the session does not exist. + Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the session's ID. - - @return IdentityApiApiExtendSessionRequest + - @return IdentityAPIApiExtendSessionRequest */ -func (a *IdentityApiService) ExtendSession(ctx context.Context, id string) IdentityApiApiExtendSessionRequest { - return IdentityApiApiExtendSessionRequest{ +func (a *IdentityAPIService) ExtendSession(ctx context.Context, id string) IdentityAPIApiExtendSessionRequest { + return IdentityAPIApiExtendSessionRequest{ ApiService: a, ctx: ctx, id: id, @@ -2093,7 +2122,7 @@ func (a *IdentityApiService) ExtendSession(ctx context.Context, id string) Ident * Execute executes the request * @return Session */ -func (a *IdentityApiService) ExtendSessionExecute(r IdentityApiApiExtendSessionRequest) (*Session, *http.Response, error) { +func (a *IdentityAPIService) ExtendSessionExecute(r IdentityAPIApiExtendSessionRequest) (*Session, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPatch localVarPostBody interface{} @@ -2103,7 +2132,7 @@ func (a *IdentityApiService) ExtendSessionExecute(r IdentityApiApiExtendSessionR localVarReturnValue *Session ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.ExtendSession") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.ExtendSession") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2156,7 +2185,7 @@ func (a *IdentityApiService) ExtendSessionExecute(r IdentityApiApiExtendSessionR return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2210,19 +2239,19 @@ func (a *IdentityApiService) ExtendSessionExecute(r IdentityApiApiExtendSessionR return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiGetIdentityRequest struct { +type IdentityAPIApiGetIdentityRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string includeCredential *[]string } -func (r IdentityApiApiGetIdentityRequest) IncludeCredential(includeCredential []string) IdentityApiApiGetIdentityRequest { +func (r IdentityAPIApiGetIdentityRequest) IncludeCredential(includeCredential []string) IdentityAPIApiGetIdentityRequest { r.includeCredential = &includeCredential return r } -func (r IdentityApiApiGetIdentityRequest) Execute() (*Identity, *http.Response, error) { +func (r IdentityAPIApiGetIdentityRequest) Execute() (*Identity, *http.Response, error) { return r.ApiService.GetIdentityExecute(r) } @@ -2233,10 +2262,10 @@ func (r IdentityApiApiGetIdentityRequest) Execute() (*Identity, *http.Response, include credentials (e.g. social sign in connections) in the response by using the `include_credential` query parameter. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID must be set to the ID of identity you want to get - - @return IdentityApiApiGetIdentityRequest + - @return IdentityAPIApiGetIdentityRequest */ -func (a *IdentityApiService) GetIdentity(ctx context.Context, id string) IdentityApiApiGetIdentityRequest { - return IdentityApiApiGetIdentityRequest{ +func (a *IdentityAPIService) GetIdentity(ctx context.Context, id string) IdentityAPIApiGetIdentityRequest { + return IdentityAPIApiGetIdentityRequest{ ApiService: a, ctx: ctx, id: id, @@ -2247,7 +2276,7 @@ func (a *IdentityApiService) GetIdentity(ctx context.Context, id string) Identit * Execute executes the request * @return Identity */ -func (a *IdentityApiService) GetIdentityExecute(r IdentityApiApiGetIdentityRequest) (*Identity, *http.Response, error) { +func (a *IdentityAPIService) GetIdentityExecute(r IdentityAPIApiGetIdentityRequest) (*Identity, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2257,7 +2286,7 @@ func (a *IdentityApiService) GetIdentityExecute(r IdentityApiApiGetIdentityReque localVarReturnValue *Identity ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.GetIdentity") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.GetIdentity") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2321,7 +2350,7 @@ func (a *IdentityApiService) GetIdentityExecute(r IdentityApiApiGetIdentityReque return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2365,13 +2394,13 @@ func (a *IdentityApiService) GetIdentityExecute(r IdentityApiApiGetIdentityReque return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiGetIdentitySchemaRequest struct { +type IdentityAPIApiGetIdentitySchemaRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string } -func (r IdentityApiApiGetIdentitySchemaRequest) Execute() (map[string]interface{}, *http.Response, error) { +func (r IdentityAPIApiGetIdentitySchemaRequest) Execute() (map[string]interface{}, *http.Response, error) { return r.ApiService.GetIdentitySchemaExecute(r) } @@ -2380,10 +2409,10 @@ func (r IdentityApiApiGetIdentitySchemaRequest) Execute() (map[string]interface{ * Return a specific identity schema. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID must be set to the ID of schema you want to get - * @return IdentityApiApiGetIdentitySchemaRequest + * @return IdentityAPIApiGetIdentitySchemaRequest */ -func (a *IdentityApiService) GetIdentitySchema(ctx context.Context, id string) IdentityApiApiGetIdentitySchemaRequest { - return IdentityApiApiGetIdentitySchemaRequest{ +func (a *IdentityAPIService) GetIdentitySchema(ctx context.Context, id string) IdentityAPIApiGetIdentitySchemaRequest { + return IdentityAPIApiGetIdentitySchemaRequest{ ApiService: a, ctx: ctx, id: id, @@ -2394,7 +2423,7 @@ func (a *IdentityApiService) GetIdentitySchema(ctx context.Context, id string) I * Execute executes the request * @return map[string]interface{} */ -func (a *IdentityApiService) GetIdentitySchemaExecute(r IdentityApiApiGetIdentitySchemaRequest) (map[string]interface{}, *http.Response, error) { +func (a *IdentityAPIService) GetIdentitySchemaExecute(r IdentityAPIApiGetIdentitySchemaRequest) (map[string]interface{}, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2404,7 +2433,7 @@ func (a *IdentityApiService) GetIdentitySchemaExecute(r IdentityApiApiGetIdentit localVarReturnValue map[string]interface{} ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.GetIdentitySchema") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.GetIdentitySchema") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2443,7 +2472,7 @@ func (a *IdentityApiService) GetIdentitySchemaExecute(r IdentityApiApiGetIdentit return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2487,19 +2516,19 @@ func (a *IdentityApiService) GetIdentitySchemaExecute(r IdentityApiApiGetIdentit return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiGetSessionRequest struct { +type IdentityAPIApiGetSessionRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string expand *[]string } -func (r IdentityApiApiGetSessionRequest) Expand(expand []string) IdentityApiApiGetSessionRequest { +func (r IdentityAPIApiGetSessionRequest) Expand(expand []string) IdentityAPIApiGetSessionRequest { r.expand = &expand return r } -func (r IdentityApiApiGetSessionRequest) Execute() (*Session, *http.Response, error) { +func (r IdentityAPIApiGetSessionRequest) Execute() (*Session, *http.Response, error) { return r.ApiService.GetSessionExecute(r) } @@ -2510,10 +2539,10 @@ func (r IdentityApiApiGetSessionRequest) Execute() (*Session, *http.Response, er Getting a session object with all specified expandables that exist in an administrative context. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the session's ID. - - @return IdentityApiApiGetSessionRequest + - @return IdentityAPIApiGetSessionRequest */ -func (a *IdentityApiService) GetSession(ctx context.Context, id string) IdentityApiApiGetSessionRequest { - return IdentityApiApiGetSessionRequest{ +func (a *IdentityAPIService) GetSession(ctx context.Context, id string) IdentityAPIApiGetSessionRequest { + return IdentityAPIApiGetSessionRequest{ ApiService: a, ctx: ctx, id: id, @@ -2524,7 +2553,7 @@ func (a *IdentityApiService) GetSession(ctx context.Context, id string) Identity * Execute executes the request * @return Session */ -func (a *IdentityApiService) GetSessionExecute(r IdentityApiApiGetSessionRequest) (*Session, *http.Response, error) { +func (a *IdentityAPIService) GetSessionExecute(r IdentityAPIApiGetSessionRequest) (*Session, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2534,7 +2563,7 @@ func (a *IdentityApiService) GetSessionExecute(r IdentityApiApiGetSessionRequest localVarReturnValue *Session ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.GetSession") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.GetSession") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2598,7 +2627,7 @@ func (a *IdentityApiService) GetSessionExecute(r IdentityApiApiGetSessionRequest return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2642,9 +2671,9 @@ func (a *IdentityApiService) GetSessionExecute(r IdentityApiApiGetSessionRequest return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiListIdentitiesRequest struct { +type IdentityAPIApiListIdentitiesRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI perPage *int64 page *int64 pageSize *int64 @@ -2653,42 +2682,47 @@ type IdentityApiApiListIdentitiesRequest struct { ids *[]string credentialsIdentifier *string previewCredentialsIdentifierSimilar *string + includeCredential *[]string } -func (r IdentityApiApiListIdentitiesRequest) PerPage(perPage int64) IdentityApiApiListIdentitiesRequest { +func (r IdentityAPIApiListIdentitiesRequest) PerPage(perPage int64) IdentityAPIApiListIdentitiesRequest { r.perPage = &perPage return r } -func (r IdentityApiApiListIdentitiesRequest) Page(page int64) IdentityApiApiListIdentitiesRequest { +func (r IdentityAPIApiListIdentitiesRequest) Page(page int64) IdentityAPIApiListIdentitiesRequest { r.page = &page return r } -func (r IdentityApiApiListIdentitiesRequest) PageSize(pageSize int64) IdentityApiApiListIdentitiesRequest { +func (r IdentityAPIApiListIdentitiesRequest) PageSize(pageSize int64) IdentityAPIApiListIdentitiesRequest { r.pageSize = &pageSize return r } -func (r IdentityApiApiListIdentitiesRequest) PageToken(pageToken string) IdentityApiApiListIdentitiesRequest { +func (r IdentityAPIApiListIdentitiesRequest) PageToken(pageToken string) IdentityAPIApiListIdentitiesRequest { r.pageToken = &pageToken return r } -func (r IdentityApiApiListIdentitiesRequest) Consistency(consistency string) IdentityApiApiListIdentitiesRequest { +func (r IdentityAPIApiListIdentitiesRequest) Consistency(consistency string) IdentityAPIApiListIdentitiesRequest { r.consistency = &consistency return r } -func (r IdentityApiApiListIdentitiesRequest) Ids(ids []string) IdentityApiApiListIdentitiesRequest { +func (r IdentityAPIApiListIdentitiesRequest) Ids(ids []string) IdentityAPIApiListIdentitiesRequest { r.ids = &ids return r } -func (r IdentityApiApiListIdentitiesRequest) CredentialsIdentifier(credentialsIdentifier string) IdentityApiApiListIdentitiesRequest { +func (r IdentityAPIApiListIdentitiesRequest) CredentialsIdentifier(credentialsIdentifier string) IdentityAPIApiListIdentitiesRequest { r.credentialsIdentifier = &credentialsIdentifier return r } -func (r IdentityApiApiListIdentitiesRequest) PreviewCredentialsIdentifierSimilar(previewCredentialsIdentifierSimilar string) IdentityApiApiListIdentitiesRequest { +func (r IdentityAPIApiListIdentitiesRequest) PreviewCredentialsIdentifierSimilar(previewCredentialsIdentifierSimilar string) IdentityAPIApiListIdentitiesRequest { r.previewCredentialsIdentifierSimilar = &previewCredentialsIdentifierSimilar return r } +func (r IdentityAPIApiListIdentitiesRequest) IncludeCredential(includeCredential []string) IdentityAPIApiListIdentitiesRequest { + r.includeCredential = &includeCredential + return r +} -func (r IdentityApiApiListIdentitiesRequest) Execute() ([]Identity, *http.Response, error) { +func (r IdentityAPIApiListIdentitiesRequest) Execute() ([]Identity, *http.Response, error) { return r.ApiService.ListIdentitiesExecute(r) } @@ -2696,10 +2730,10 @@ func (r IdentityApiApiListIdentitiesRequest) Execute() ([]Identity, *http.Respon * ListIdentities List Identities * Lists all [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model) in the system. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiListIdentitiesRequest + * @return IdentityAPIApiListIdentitiesRequest */ -func (a *IdentityApiService) ListIdentities(ctx context.Context) IdentityApiApiListIdentitiesRequest { - return IdentityApiApiListIdentitiesRequest{ +func (a *IdentityAPIService) ListIdentities(ctx context.Context) IdentityAPIApiListIdentitiesRequest { + return IdentityAPIApiListIdentitiesRequest{ ApiService: a, ctx: ctx, } @@ -2709,7 +2743,7 @@ func (a *IdentityApiService) ListIdentities(ctx context.Context) IdentityApiApiL * Execute executes the request * @return []Identity */ -func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitiesRequest) ([]Identity, *http.Response, error) { +func (a *IdentityAPIService) ListIdentitiesExecute(r IdentityAPIApiListIdentitiesRequest) ([]Identity, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2719,7 +2753,7 @@ func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitie localVarReturnValue []Identity ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.ListIdentities") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.ListIdentities") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2762,6 +2796,17 @@ func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitie if r.previewCredentialsIdentifierSimilar != nil { localVarQueryParams.Add("preview_credentials_identifier_similar", parameterToString(*r.previewCredentialsIdentifierSimilar, "")) } + if r.includeCredential != nil { + t := *r.includeCredential + if reflect.TypeOf(t).Kind() == reflect.Slice { + s := reflect.ValueOf(t) + for i := 0; i < s.Len(); i++ { + localVarQueryParams.Add("include_credential", parameterToString(s.Index(i), "multi")) + } + } else { + localVarQueryParams.Add("include_credential", parameterToString(t, "multi")) + } + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} @@ -2803,7 +2848,7 @@ func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitie return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2837,33 +2882,33 @@ func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitie return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiListIdentitySchemasRequest struct { +type IdentityAPIApiListIdentitySchemasRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI perPage *int64 page *int64 pageSize *int64 pageToken *string } -func (r IdentityApiApiListIdentitySchemasRequest) PerPage(perPage int64) IdentityApiApiListIdentitySchemasRequest { +func (r IdentityAPIApiListIdentitySchemasRequest) PerPage(perPage int64) IdentityAPIApiListIdentitySchemasRequest { r.perPage = &perPage return r } -func (r IdentityApiApiListIdentitySchemasRequest) Page(page int64) IdentityApiApiListIdentitySchemasRequest { +func (r IdentityAPIApiListIdentitySchemasRequest) Page(page int64) IdentityAPIApiListIdentitySchemasRequest { r.page = &page return r } -func (r IdentityApiApiListIdentitySchemasRequest) PageSize(pageSize int64) IdentityApiApiListIdentitySchemasRequest { +func (r IdentityAPIApiListIdentitySchemasRequest) PageSize(pageSize int64) IdentityAPIApiListIdentitySchemasRequest { r.pageSize = &pageSize return r } -func (r IdentityApiApiListIdentitySchemasRequest) PageToken(pageToken string) IdentityApiApiListIdentitySchemasRequest { +func (r IdentityAPIApiListIdentitySchemasRequest) PageToken(pageToken string) IdentityAPIApiListIdentitySchemasRequest { r.pageToken = &pageToken return r } -func (r IdentityApiApiListIdentitySchemasRequest) Execute() ([]IdentitySchemaContainer, *http.Response, error) { +func (r IdentityAPIApiListIdentitySchemasRequest) Execute() ([]IdentitySchemaContainer, *http.Response, error) { return r.ApiService.ListIdentitySchemasExecute(r) } @@ -2871,10 +2916,10 @@ func (r IdentityApiApiListIdentitySchemasRequest) Execute() ([]IdentitySchemaCon * ListIdentitySchemas Get all Identity Schemas * Returns a list of all identity schemas currently in use. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiListIdentitySchemasRequest + * @return IdentityAPIApiListIdentitySchemasRequest */ -func (a *IdentityApiService) ListIdentitySchemas(ctx context.Context) IdentityApiApiListIdentitySchemasRequest { - return IdentityApiApiListIdentitySchemasRequest{ +func (a *IdentityAPIService) ListIdentitySchemas(ctx context.Context) IdentityAPIApiListIdentitySchemasRequest { + return IdentityAPIApiListIdentitySchemasRequest{ ApiService: a, ctx: ctx, } @@ -2884,7 +2929,7 @@ func (a *IdentityApiService) ListIdentitySchemas(ctx context.Context) IdentityAp * Execute executes the request * @return []IdentitySchemaContainer */ -func (a *IdentityApiService) ListIdentitySchemasExecute(r IdentityApiApiListIdentitySchemasRequest) ([]IdentitySchemaContainer, *http.Response, error) { +func (a *IdentityAPIService) ListIdentitySchemasExecute(r IdentityAPIApiListIdentitySchemasRequest) ([]IdentitySchemaContainer, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2894,7 +2939,7 @@ func (a *IdentityApiService) ListIdentitySchemasExecute(r IdentityApiApiListIden localVarReturnValue []IdentitySchemaContainer ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.ListIdentitySchemas") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.ListIdentitySchemas") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2944,7 +2989,7 @@ func (a *IdentityApiService) ListIdentitySchemasExecute(r IdentityApiApiListIden return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2978,9 +3023,9 @@ func (a *IdentityApiService) ListIdentitySchemasExecute(r IdentityApiApiListIden return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiListIdentitySessionsRequest struct { +type IdentityAPIApiListIdentitySessionsRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string perPage *int64 page *int64 @@ -2989,28 +3034,28 @@ type IdentityApiApiListIdentitySessionsRequest struct { active *bool } -func (r IdentityApiApiListIdentitySessionsRequest) PerPage(perPage int64) IdentityApiApiListIdentitySessionsRequest { +func (r IdentityAPIApiListIdentitySessionsRequest) PerPage(perPage int64) IdentityAPIApiListIdentitySessionsRequest { r.perPage = &perPage return r } -func (r IdentityApiApiListIdentitySessionsRequest) Page(page int64) IdentityApiApiListIdentitySessionsRequest { +func (r IdentityAPIApiListIdentitySessionsRequest) Page(page int64) IdentityAPIApiListIdentitySessionsRequest { r.page = &page return r } -func (r IdentityApiApiListIdentitySessionsRequest) PageSize(pageSize int64) IdentityApiApiListIdentitySessionsRequest { +func (r IdentityAPIApiListIdentitySessionsRequest) PageSize(pageSize int64) IdentityAPIApiListIdentitySessionsRequest { r.pageSize = &pageSize return r } -func (r IdentityApiApiListIdentitySessionsRequest) PageToken(pageToken string) IdentityApiApiListIdentitySessionsRequest { +func (r IdentityAPIApiListIdentitySessionsRequest) PageToken(pageToken string) IdentityAPIApiListIdentitySessionsRequest { r.pageToken = &pageToken return r } -func (r IdentityApiApiListIdentitySessionsRequest) Active(active bool) IdentityApiApiListIdentitySessionsRequest { +func (r IdentityAPIApiListIdentitySessionsRequest) Active(active bool) IdentityAPIApiListIdentitySessionsRequest { r.active = &active return r } -func (r IdentityApiApiListIdentitySessionsRequest) Execute() ([]Session, *http.Response, error) { +func (r IdentityAPIApiListIdentitySessionsRequest) Execute() ([]Session, *http.Response, error) { return r.ApiService.ListIdentitySessionsExecute(r) } @@ -3019,10 +3064,10 @@ func (r IdentityApiApiListIdentitySessionsRequest) Execute() ([]Session, *http.R * This endpoint returns all sessions that belong to the given Identity. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @return IdentityApiApiListIdentitySessionsRequest + * @return IdentityAPIApiListIdentitySessionsRequest */ -func (a *IdentityApiService) ListIdentitySessions(ctx context.Context, id string) IdentityApiApiListIdentitySessionsRequest { - return IdentityApiApiListIdentitySessionsRequest{ +func (a *IdentityAPIService) ListIdentitySessions(ctx context.Context, id string) IdentityAPIApiListIdentitySessionsRequest { + return IdentityAPIApiListIdentitySessionsRequest{ ApiService: a, ctx: ctx, id: id, @@ -3033,7 +3078,7 @@ func (a *IdentityApiService) ListIdentitySessions(ctx context.Context, id string * Execute executes the request * @return []Session */ -func (a *IdentityApiService) ListIdentitySessionsExecute(r IdentityApiApiListIdentitySessionsRequest) ([]Session, *http.Response, error) { +func (a *IdentityAPIService) ListIdentitySessionsExecute(r IdentityAPIApiListIdentitySessionsRequest) ([]Session, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -3043,7 +3088,7 @@ func (a *IdentityApiService) ListIdentitySessionsExecute(r IdentityApiApiListIde localVarReturnValue []Session ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.ListIdentitySessions") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.ListIdentitySessions") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3111,7 +3156,7 @@ func (a *IdentityApiService) ListIdentitySessionsExecute(r IdentityApiApiListIde return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3165,33 +3210,33 @@ func (a *IdentityApiService) ListIdentitySessionsExecute(r IdentityApiApiListIde return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiListSessionsRequest struct { +type IdentityAPIApiListSessionsRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI pageSize *int64 pageToken *string active *bool expand *[]string } -func (r IdentityApiApiListSessionsRequest) PageSize(pageSize int64) IdentityApiApiListSessionsRequest { +func (r IdentityAPIApiListSessionsRequest) PageSize(pageSize int64) IdentityAPIApiListSessionsRequest { r.pageSize = &pageSize return r } -func (r IdentityApiApiListSessionsRequest) PageToken(pageToken string) IdentityApiApiListSessionsRequest { +func (r IdentityAPIApiListSessionsRequest) PageToken(pageToken string) IdentityAPIApiListSessionsRequest { r.pageToken = &pageToken return r } -func (r IdentityApiApiListSessionsRequest) Active(active bool) IdentityApiApiListSessionsRequest { +func (r IdentityAPIApiListSessionsRequest) Active(active bool) IdentityAPIApiListSessionsRequest { r.active = &active return r } -func (r IdentityApiApiListSessionsRequest) Expand(expand []string) IdentityApiApiListSessionsRequest { +func (r IdentityAPIApiListSessionsRequest) Expand(expand []string) IdentityAPIApiListSessionsRequest { r.expand = &expand return r } -func (r IdentityApiApiListSessionsRequest) Execute() ([]Session, *http.Response, error) { +func (r IdentityAPIApiListSessionsRequest) Execute() ([]Session, *http.Response, error) { return r.ApiService.ListSessionsExecute(r) } @@ -3199,10 +3244,10 @@ func (r IdentityApiApiListSessionsRequest) Execute() ([]Session, *http.Response, * ListSessions List All Sessions * Listing all sessions that exist. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiListSessionsRequest + * @return IdentityAPIApiListSessionsRequest */ -func (a *IdentityApiService) ListSessions(ctx context.Context) IdentityApiApiListSessionsRequest { - return IdentityApiApiListSessionsRequest{ +func (a *IdentityAPIService) ListSessions(ctx context.Context) IdentityAPIApiListSessionsRequest { + return IdentityAPIApiListSessionsRequest{ ApiService: a, ctx: ctx, } @@ -3212,7 +3257,7 @@ func (a *IdentityApiService) ListSessions(ctx context.Context) IdentityApiApiLis * Execute executes the request * @return []Session */ -func (a *IdentityApiService) ListSessionsExecute(r IdentityApiApiListSessionsRequest) ([]Session, *http.Response, error) { +func (a *IdentityAPIService) ListSessionsExecute(r IdentityAPIApiListSessionsRequest) ([]Session, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -3222,7 +3267,7 @@ func (a *IdentityApiService) ListSessionsExecute(r IdentityApiApiListSessionsReq localVarReturnValue []Session ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.ListSessions") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.ListSessions") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3294,7 +3339,7 @@ func (a *IdentityApiService) ListSessionsExecute(r IdentityApiApiListSessionsReq return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3338,19 +3383,19 @@ func (a *IdentityApiService) ListSessionsExecute(r IdentityApiApiListSessionsReq return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiPatchIdentityRequest struct { +type IdentityAPIApiPatchIdentityRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string jsonPatch *[]JsonPatch } -func (r IdentityApiApiPatchIdentityRequest) JsonPatch(jsonPatch []JsonPatch) IdentityApiApiPatchIdentityRequest { +func (r IdentityAPIApiPatchIdentityRequest) JsonPatch(jsonPatch []JsonPatch) IdentityAPIApiPatchIdentityRequest { r.jsonPatch = &jsonPatch return r } -func (r IdentityApiApiPatchIdentityRequest) Execute() (*Identity, *http.Response, error) { +func (r IdentityAPIApiPatchIdentityRequest) Execute() (*Identity, *http.Response, error) { return r.ApiService.PatchIdentityExecute(r) } @@ -3361,10 +3406,10 @@ func (r IdentityApiApiPatchIdentityRequest) Execute() (*Identity, *http.Response The fields `id`, `stateChangedAt` and `credentials` can not be updated using this method. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID must be set to the ID of identity you want to update - - @return IdentityApiApiPatchIdentityRequest + - @return IdentityAPIApiPatchIdentityRequest */ -func (a *IdentityApiService) PatchIdentity(ctx context.Context, id string) IdentityApiApiPatchIdentityRequest { - return IdentityApiApiPatchIdentityRequest{ +func (a *IdentityAPIService) PatchIdentity(ctx context.Context, id string) IdentityAPIApiPatchIdentityRequest { + return IdentityAPIApiPatchIdentityRequest{ ApiService: a, ctx: ctx, id: id, @@ -3375,7 +3420,7 @@ func (a *IdentityApiService) PatchIdentity(ctx context.Context, id string) Ident * Execute executes the request * @return Identity */ -func (a *IdentityApiService) PatchIdentityExecute(r IdentityApiApiPatchIdentityRequest) (*Identity, *http.Response, error) { +func (a *IdentityAPIService) PatchIdentityExecute(r IdentityAPIApiPatchIdentityRequest) (*Identity, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPatch localVarPostBody interface{} @@ -3385,7 +3430,7 @@ func (a *IdentityApiService) PatchIdentityExecute(r IdentityApiApiPatchIdentityR localVarReturnValue *Identity ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.PatchIdentity") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.PatchIdentity") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3440,7 +3485,7 @@ func (a *IdentityApiService) PatchIdentityExecute(r IdentityApiApiPatchIdentityR return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3504,19 +3549,19 @@ func (a *IdentityApiService) PatchIdentityExecute(r IdentityApiApiPatchIdentityR return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiUpdateIdentityRequest struct { +type IdentityAPIApiUpdateIdentityRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string updateIdentityBody *UpdateIdentityBody } -func (r IdentityApiApiUpdateIdentityRequest) UpdateIdentityBody(updateIdentityBody UpdateIdentityBody) IdentityApiApiUpdateIdentityRequest { +func (r IdentityAPIApiUpdateIdentityRequest) UpdateIdentityBody(updateIdentityBody UpdateIdentityBody) IdentityAPIApiUpdateIdentityRequest { r.updateIdentityBody = &updateIdentityBody return r } -func (r IdentityApiApiUpdateIdentityRequest) Execute() (*Identity, *http.Response, error) { +func (r IdentityAPIApiUpdateIdentityRequest) Execute() (*Identity, *http.Response, error) { return r.ApiService.UpdateIdentityExecute(r) } @@ -3527,10 +3572,10 @@ func (r IdentityApiApiUpdateIdentityRequest) Execute() (*Identity, *http.Respons payload (except credentials) is expected. It is possible to update the identity's credentials as well. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID must be set to the ID of identity you want to update - - @return IdentityApiApiUpdateIdentityRequest + - @return IdentityAPIApiUpdateIdentityRequest */ -func (a *IdentityApiService) UpdateIdentity(ctx context.Context, id string) IdentityApiApiUpdateIdentityRequest { - return IdentityApiApiUpdateIdentityRequest{ +func (a *IdentityAPIService) UpdateIdentity(ctx context.Context, id string) IdentityAPIApiUpdateIdentityRequest { + return IdentityAPIApiUpdateIdentityRequest{ ApiService: a, ctx: ctx, id: id, @@ -3541,7 +3586,7 @@ func (a *IdentityApiService) UpdateIdentity(ctx context.Context, id string) Iden * Execute executes the request * @return Identity */ -func (a *IdentityApiService) UpdateIdentityExecute(r IdentityApiApiUpdateIdentityRequest) (*Identity, *http.Response, error) { +func (a *IdentityAPIService) UpdateIdentityExecute(r IdentityAPIApiUpdateIdentityRequest) (*Identity, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPut localVarPostBody interface{} @@ -3551,7 +3596,7 @@ func (a *IdentityApiService) UpdateIdentityExecute(r IdentityApiApiUpdateIdentit localVarReturnValue *Identity ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.UpdateIdentity") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.UpdateIdentity") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3606,7 +3651,7 @@ func (a *IdentityApiService) UpdateIdentityExecute(r IdentityApiApiUpdateIdentit return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { diff --git a/internal/client-go/api_metadata.go b/internal/client-go/api_metadata.go index 0ea297189e2f..4bef0d5cb6ca 100644 --- a/internal/client-go/api_metadata.go +++ b/internal/client-go/api_metadata.go @@ -24,7 +24,7 @@ var ( _ context.Context ) -type MetadataApi interface { +type MetadataAPI interface { /* * GetVersion Return Running Software Version. @@ -36,15 +36,15 @@ type MetadataApi interface { Be aware that if you are running multiple nodes of this service, the version will never refer to the cluster state, only to a single instance. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return MetadataApiApiGetVersionRequest + * @return MetadataAPIApiGetVersionRequest */ - GetVersion(ctx context.Context) MetadataApiApiGetVersionRequest + GetVersion(ctx context.Context) MetadataAPIApiGetVersionRequest /* * GetVersionExecute executes the request * @return GetVersion200Response */ - GetVersionExecute(r MetadataApiApiGetVersionRequest) (*GetVersion200Response, *http.Response, error) + GetVersionExecute(r MetadataAPIApiGetVersionRequest) (*GetVersion200Response, *http.Response, error) /* * IsAlive Check HTTP Server Status @@ -57,15 +57,15 @@ type MetadataApi interface { Be aware that if you are running multiple nodes of this service, the health status will never refer to the cluster state, only to a single instance. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return MetadataApiApiIsAliveRequest + * @return MetadataAPIApiIsAliveRequest */ - IsAlive(ctx context.Context) MetadataApiApiIsAliveRequest + IsAlive(ctx context.Context) MetadataAPIApiIsAliveRequest /* * IsAliveExecute executes the request * @return IsAlive200Response */ - IsAliveExecute(r MetadataApiApiIsAliveRequest) (*IsAlive200Response, *http.Response, error) + IsAliveExecute(r MetadataAPIApiIsAliveRequest) (*IsAlive200Response, *http.Response, error) /* * IsReady Check HTTP Server and Database Status @@ -78,26 +78,26 @@ type MetadataApi interface { Be aware that if you are running multiple nodes of Ory Kratos, the health status will never refer to the cluster state, only to a single instance. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return MetadataApiApiIsReadyRequest + * @return MetadataAPIApiIsReadyRequest */ - IsReady(ctx context.Context) MetadataApiApiIsReadyRequest + IsReady(ctx context.Context) MetadataAPIApiIsReadyRequest /* * IsReadyExecute executes the request * @return IsAlive200Response */ - IsReadyExecute(r MetadataApiApiIsReadyRequest) (*IsAlive200Response, *http.Response, error) + IsReadyExecute(r MetadataAPIApiIsReadyRequest) (*IsAlive200Response, *http.Response, error) } -// MetadataApiService MetadataApi service -type MetadataApiService service +// MetadataAPIService MetadataAPI service +type MetadataAPIService service -type MetadataApiApiGetVersionRequest struct { +type MetadataAPIApiGetVersionRequest struct { ctx context.Context - ApiService MetadataApi + ApiService MetadataAPI } -func (r MetadataApiApiGetVersionRequest) Execute() (*GetVersion200Response, *http.Response, error) { +func (r MetadataAPIApiGetVersionRequest) Execute() (*GetVersion200Response, *http.Response, error) { return r.ApiService.GetVersionExecute(r) } @@ -111,10 +111,10 @@ If the service supports TLS Edge Termination, this endpoint does not require the Be aware that if you are running multiple nodes of this service, the version will never refer to the cluster state, only to a single instance. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return MetadataApiApiGetVersionRequest + - @return MetadataAPIApiGetVersionRequest */ -func (a *MetadataApiService) GetVersion(ctx context.Context) MetadataApiApiGetVersionRequest { - return MetadataApiApiGetVersionRequest{ +func (a *MetadataAPIService) GetVersion(ctx context.Context) MetadataAPIApiGetVersionRequest { + return MetadataAPIApiGetVersionRequest{ ApiService: a, ctx: ctx, } @@ -124,7 +124,7 @@ func (a *MetadataApiService) GetVersion(ctx context.Context) MetadataApiApiGetVe * Execute executes the request * @return GetVersion200Response */ -func (a *MetadataApiService) GetVersionExecute(r MetadataApiApiGetVersionRequest) (*GetVersion200Response, *http.Response, error) { +func (a *MetadataAPIService) GetVersionExecute(r MetadataAPIApiGetVersionRequest) (*GetVersion200Response, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -134,7 +134,7 @@ func (a *MetadataApiService) GetVersionExecute(r MetadataApiApiGetVersionRequest localVarReturnValue *GetVersion200Response ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataApiService.GetVersion") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataAPIService.GetVersion") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -172,7 +172,7 @@ func (a *MetadataApiService) GetVersionExecute(r MetadataApiApiGetVersionRequest return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -199,12 +199,12 @@ func (a *MetadataApiService) GetVersionExecute(r MetadataApiApiGetVersionRequest return localVarReturnValue, localVarHTTPResponse, nil } -type MetadataApiApiIsAliveRequest struct { +type MetadataAPIApiIsAliveRequest struct { ctx context.Context - ApiService MetadataApi + ApiService MetadataAPI } -func (r MetadataApiApiIsAliveRequest) Execute() (*IsAlive200Response, *http.Response, error) { +func (r MetadataAPIApiIsAliveRequest) Execute() (*IsAlive200Response, *http.Response, error) { return r.ApiService.IsAliveExecute(r) } @@ -220,10 +220,10 @@ If the service supports TLS Edge Termination, this endpoint does not require the Be aware that if you are running multiple nodes of this service, the health status will never refer to the cluster state, only to a single instance. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return MetadataApiApiIsAliveRequest + - @return MetadataAPIApiIsAliveRequest */ -func (a *MetadataApiService) IsAlive(ctx context.Context) MetadataApiApiIsAliveRequest { - return MetadataApiApiIsAliveRequest{ +func (a *MetadataAPIService) IsAlive(ctx context.Context) MetadataAPIApiIsAliveRequest { + return MetadataAPIApiIsAliveRequest{ ApiService: a, ctx: ctx, } @@ -233,7 +233,7 @@ func (a *MetadataApiService) IsAlive(ctx context.Context) MetadataApiApiIsAliveR * Execute executes the request * @return IsAlive200Response */ -func (a *MetadataApiService) IsAliveExecute(r MetadataApiApiIsAliveRequest) (*IsAlive200Response, *http.Response, error) { +func (a *MetadataAPIService) IsAliveExecute(r MetadataAPIApiIsAliveRequest) (*IsAlive200Response, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -243,7 +243,7 @@ func (a *MetadataApiService) IsAliveExecute(r MetadataApiApiIsAliveRequest) (*Is localVarReturnValue *IsAlive200Response ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataApiService.IsAlive") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataAPIService.IsAlive") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -281,7 +281,7 @@ func (a *MetadataApiService) IsAliveExecute(r MetadataApiApiIsAliveRequest) (*Is return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -315,12 +315,12 @@ func (a *MetadataApiService) IsAliveExecute(r MetadataApiApiIsAliveRequest) (*Is return localVarReturnValue, localVarHTTPResponse, nil } -type MetadataApiApiIsReadyRequest struct { +type MetadataAPIApiIsReadyRequest struct { ctx context.Context - ApiService MetadataApi + ApiService MetadataAPI } -func (r MetadataApiApiIsReadyRequest) Execute() (*IsAlive200Response, *http.Response, error) { +func (r MetadataAPIApiIsReadyRequest) Execute() (*IsAlive200Response, *http.Response, error) { return r.ApiService.IsReadyExecute(r) } @@ -336,10 +336,10 @@ If the service supports TLS Edge Termination, this endpoint does not require the Be aware that if you are running multiple nodes of Ory Kratos, the health status will never refer to the cluster state, only to a single instance. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return MetadataApiApiIsReadyRequest + - @return MetadataAPIApiIsReadyRequest */ -func (a *MetadataApiService) IsReady(ctx context.Context) MetadataApiApiIsReadyRequest { - return MetadataApiApiIsReadyRequest{ +func (a *MetadataAPIService) IsReady(ctx context.Context) MetadataAPIApiIsReadyRequest { + return MetadataAPIApiIsReadyRequest{ ApiService: a, ctx: ctx, } @@ -349,7 +349,7 @@ func (a *MetadataApiService) IsReady(ctx context.Context) MetadataApiApiIsReadyR * Execute executes the request * @return IsAlive200Response */ -func (a *MetadataApiService) IsReadyExecute(r MetadataApiApiIsReadyRequest) (*IsAlive200Response, *http.Response, error) { +func (a *MetadataAPIService) IsReadyExecute(r MetadataAPIApiIsReadyRequest) (*IsAlive200Response, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -359,7 +359,7 @@ func (a *MetadataApiService) IsReadyExecute(r MetadataApiApiIsReadyRequest) (*Is localVarReturnValue *IsAlive200Response ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataApiService.IsReady") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataAPIService.IsReady") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -397,7 +397,7 @@ func (a *MetadataApiService) IsReadyExecute(r MetadataApiApiIsReadyRequest) (*Is return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { diff --git a/internal/client-go/client.go b/internal/client-go/client.go index 949a0e51ab35..14ee5d7619a6 100644 --- a/internal/client-go/client.go +++ b/internal/client-go/client.go @@ -49,13 +49,13 @@ type APIClient struct { // API Services - CourierApi CourierApi + CourierAPI CourierAPI - FrontendApi FrontendApi + FrontendAPI FrontendAPI - IdentityApi IdentityApi + IdentityAPI IdentityAPI - MetadataApi MetadataApi + MetadataAPI MetadataAPI } type service struct { @@ -74,10 +74,10 @@ func NewAPIClient(cfg *Configuration) *APIClient { c.common.client = c // API Services - c.CourierApi = (*CourierApiService)(&c.common) - c.FrontendApi = (*FrontendApiService)(&c.common) - c.IdentityApi = (*IdentityApiService)(&c.common) - c.MetadataApi = (*MetadataApiService)(&c.common) + c.CourierAPI = (*CourierAPIService)(&c.common) + c.FrontendAPI = (*FrontendAPIService)(&c.common) + c.IdentityAPI = (*IdentityAPIService)(&c.common) + c.MetadataAPI = (*MetadataAPIService)(&c.common) return c } diff --git a/internal/client-go/model_continue_with.go b/internal/client-go/model_continue_with.go index ee84e74692fb..6fb1056836e6 100644 --- a/internal/client-go/model_continue_with.go +++ b/internal/client-go/model_continue_with.go @@ -19,6 +19,7 @@ import ( // ContinueWith - struct for ContinueWith type ContinueWith struct { ContinueWithRecoveryUi *ContinueWithRecoveryUi + ContinueWithRedirectBrowserTo *ContinueWithRedirectBrowserTo ContinueWithSetOrySessionToken *ContinueWithSetOrySessionToken ContinueWithSettingsUi *ContinueWithSettingsUi ContinueWithVerificationUi *ContinueWithVerificationUi @@ -31,6 +32,13 @@ func ContinueWithRecoveryUiAsContinueWith(v *ContinueWithRecoveryUi) ContinueWit } } +// ContinueWithRedirectBrowserToAsContinueWith is a convenience function that returns ContinueWithRedirectBrowserTo wrapped in ContinueWith +func ContinueWithRedirectBrowserToAsContinueWith(v *ContinueWithRedirectBrowserTo) ContinueWith { + return ContinueWith{ + ContinueWithRedirectBrowserTo: v, + } +} + // ContinueWithSetOrySessionTokenAsContinueWith is a convenience function that returns ContinueWithSetOrySessionToken wrapped in ContinueWith func ContinueWithSetOrySessionTokenAsContinueWith(v *ContinueWithSetOrySessionToken) ContinueWith { return ContinueWith{ @@ -55,72 +63,134 @@ func ContinueWithVerificationUiAsContinueWith(v *ContinueWithVerificationUi) Con // Unmarshal JSON data into one of the pointers in the struct func (dst *ContinueWith) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into ContinueWithRecoveryUi - err = newStrictDecoder(data).Decode(&dst.ContinueWithRecoveryUi) - if err == nil { - jsonContinueWithRecoveryUi, _ := json.Marshal(dst.ContinueWithRecoveryUi) - if string(jsonContinueWithRecoveryUi) == "{}" { // empty struct - dst.ContinueWithRecoveryUi = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'redirect_browser_to' + if jsonDict["action"] == "redirect_browser_to" { + // try to unmarshal JSON data into ContinueWithRedirectBrowserTo + err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo) + if err == nil { + return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match } else { - match++ + dst.ContinueWithRedirectBrowserTo = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error()) } - } else { - dst.ContinueWithRecoveryUi = nil } - // try to unmarshal data into ContinueWithSetOrySessionToken - err = newStrictDecoder(data).Decode(&dst.ContinueWithSetOrySessionToken) - if err == nil { - jsonContinueWithSetOrySessionToken, _ := json.Marshal(dst.ContinueWithSetOrySessionToken) - if string(jsonContinueWithSetOrySessionToken) == "{}" { // empty struct + // check if the discriminator value is 'set_ory_session_token' + if jsonDict["action"] == "set_ory_session_token" { + // try to unmarshal JSON data into ContinueWithSetOrySessionToken + err = json.Unmarshal(data, &dst.ContinueWithSetOrySessionToken) + if err == nil { + return nil // data stored in dst.ContinueWithSetOrySessionToken, return on the first match + } else { dst.ContinueWithSetOrySessionToken = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithSetOrySessionToken: %s", err.Error()) + } + } + + // check if the discriminator value is 'show_recovery_ui' + if jsonDict["action"] == "show_recovery_ui" { + // try to unmarshal JSON data into ContinueWithRecoveryUi + err = json.Unmarshal(data, &dst.ContinueWithRecoveryUi) + if err == nil { + return nil // data stored in dst.ContinueWithRecoveryUi, return on the first match } else { - match++ + dst.ContinueWithRecoveryUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRecoveryUi: %s", err.Error()) } - } else { - dst.ContinueWithSetOrySessionToken = nil } - // try to unmarshal data into ContinueWithSettingsUi - err = newStrictDecoder(data).Decode(&dst.ContinueWithSettingsUi) - if err == nil { - jsonContinueWithSettingsUi, _ := json.Marshal(dst.ContinueWithSettingsUi) - if string(jsonContinueWithSettingsUi) == "{}" { // empty struct - dst.ContinueWithSettingsUi = nil + // check if the discriminator value is 'show_settings_ui' + if jsonDict["action"] == "show_settings_ui" { + // try to unmarshal JSON data into ContinueWithSettingsUi + err = json.Unmarshal(data, &dst.ContinueWithSettingsUi) + if err == nil { + return nil // data stored in dst.ContinueWithSettingsUi, return on the first match } else { - match++ + dst.ContinueWithSettingsUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithSettingsUi: %s", err.Error()) } - } else { - dst.ContinueWithSettingsUi = nil } - // try to unmarshal data into ContinueWithVerificationUi - err = newStrictDecoder(data).Decode(&dst.ContinueWithVerificationUi) - if err == nil { - jsonContinueWithVerificationUi, _ := json.Marshal(dst.ContinueWithVerificationUi) - if string(jsonContinueWithVerificationUi) == "{}" { // empty struct + // check if the discriminator value is 'show_verification_ui' + if jsonDict["action"] == "show_verification_ui" { + // try to unmarshal JSON data into ContinueWithVerificationUi + err = json.Unmarshal(data, &dst.ContinueWithVerificationUi) + if err == nil { + return nil // data stored in dst.ContinueWithVerificationUi, return on the first match + } else { dst.ContinueWithVerificationUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithVerificationUi: %s", err.Error()) + } + } + + // check if the discriminator value is 'continueWithRecoveryUi' + if jsonDict["action"] == "continueWithRecoveryUi" { + // try to unmarshal JSON data into ContinueWithRecoveryUi + err = json.Unmarshal(data, &dst.ContinueWithRecoveryUi) + if err == nil { + return nil // data stored in dst.ContinueWithRecoveryUi, return on the first match } else { - match++ + dst.ContinueWithRecoveryUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRecoveryUi: %s", err.Error()) } - } else { - dst.ContinueWithVerificationUi = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.ContinueWithRecoveryUi = nil - dst.ContinueWithSetOrySessionToken = nil - dst.ContinueWithSettingsUi = nil - dst.ContinueWithVerificationUi = nil + // check if the discriminator value is 'continueWithRedirectBrowserTo' + if jsonDict["action"] == "continueWithRedirectBrowserTo" { + // try to unmarshal JSON data into ContinueWithRedirectBrowserTo + err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo) + if err == nil { + return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match + } else { + dst.ContinueWithRedirectBrowserTo = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error()) + } + } + + // check if the discriminator value is 'continueWithSetOrySessionToken' + if jsonDict["action"] == "continueWithSetOrySessionToken" { + // try to unmarshal JSON data into ContinueWithSetOrySessionToken + err = json.Unmarshal(data, &dst.ContinueWithSetOrySessionToken) + if err == nil { + return nil // data stored in dst.ContinueWithSetOrySessionToken, return on the first match + } else { + dst.ContinueWithSetOrySessionToken = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithSetOrySessionToken: %s", err.Error()) + } + } + + // check if the discriminator value is 'continueWithSettingsUi' + if jsonDict["action"] == "continueWithSettingsUi" { + // try to unmarshal JSON data into ContinueWithSettingsUi + err = json.Unmarshal(data, &dst.ContinueWithSettingsUi) + if err == nil { + return nil // data stored in dst.ContinueWithSettingsUi, return on the first match + } else { + dst.ContinueWithSettingsUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithSettingsUi: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(ContinueWith)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(ContinueWith)") + // check if the discriminator value is 'continueWithVerificationUi' + if jsonDict["action"] == "continueWithVerificationUi" { + // try to unmarshal JSON data into ContinueWithVerificationUi + err = json.Unmarshal(data, &dst.ContinueWithVerificationUi) + if err == nil { + return nil // data stored in dst.ContinueWithVerificationUi, return on the first match + } else { + dst.ContinueWithVerificationUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithVerificationUi: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON @@ -129,6 +199,10 @@ func (src ContinueWith) MarshalJSON() ([]byte, error) { return json.Marshal(&src.ContinueWithRecoveryUi) } + if src.ContinueWithRedirectBrowserTo != nil { + return json.Marshal(&src.ContinueWithRedirectBrowserTo) + } + if src.ContinueWithSetOrySessionToken != nil { return json.Marshal(&src.ContinueWithSetOrySessionToken) } @@ -153,6 +227,10 @@ func (obj *ContinueWith) GetActualInstance() interface{} { return obj.ContinueWithRecoveryUi } + if obj.ContinueWithRedirectBrowserTo != nil { + return obj.ContinueWithRedirectBrowserTo + } + if obj.ContinueWithSetOrySessionToken != nil { return obj.ContinueWithSetOrySessionToken } diff --git a/internal/client-go/model_continue_with_recovery_ui_flow.go b/internal/client-go/model_continue_with_recovery_ui_flow.go index 3fde7e717ef2..251725a73c3b 100644 --- a/internal/client-go/model_continue_with_recovery_ui_flow.go +++ b/internal/client-go/model_continue_with_recovery_ui_flow.go @@ -19,7 +19,7 @@ import ( type ContinueWithRecoveryUiFlow struct { // The ID of the recovery flow Id string `json:"id"` - // The URL of the recovery flow + // The URL of the recovery flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. Url *string `json:"url,omitempty"` } diff --git a/internal/client-go/model_continue_with_redirect_browser_to.go b/internal/client-go/model_continue_with_redirect_browser_to.go new file mode 100644 index 000000000000..20c3e4f3c562 --- /dev/null +++ b/internal/client-go/model_continue_with_redirect_browser_to.go @@ -0,0 +1,138 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// ContinueWithRedirectBrowserTo Indicates, that the UI flow could be continued by showing a recovery ui +type ContinueWithRedirectBrowserTo struct { + // Action will always be `redirect_browser_to` redirect_browser_to ContinueWithActionRedirectBrowserToString + Action string `json:"action"` + // The URL to redirect the browser to + RedirectBrowserTo string `json:"redirect_browser_to"` +} + +// NewContinueWithRedirectBrowserTo instantiates a new ContinueWithRedirectBrowserTo object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewContinueWithRedirectBrowserTo(action string, redirectBrowserTo string) *ContinueWithRedirectBrowserTo { + this := ContinueWithRedirectBrowserTo{} + this.Action = action + this.RedirectBrowserTo = redirectBrowserTo + return &this +} + +// NewContinueWithRedirectBrowserToWithDefaults instantiates a new ContinueWithRedirectBrowserTo object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewContinueWithRedirectBrowserToWithDefaults() *ContinueWithRedirectBrowserTo { + this := ContinueWithRedirectBrowserTo{} + return &this +} + +// GetAction returns the Action field value +func (o *ContinueWithRedirectBrowserTo) GetAction() string { + if o == nil { + var ret string + return ret + } + + return o.Action +} + +// GetActionOk returns a tuple with the Action field value +// and a boolean to check if the value has been set. +func (o *ContinueWithRedirectBrowserTo) GetActionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *ContinueWithRedirectBrowserTo) SetAction(v string) { + o.Action = v +} + +// GetRedirectBrowserTo returns the RedirectBrowserTo field value +func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserTo() string { + if o == nil { + var ret string + return ret + } + + return o.RedirectBrowserTo +} + +// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value +// and a boolean to check if the value has been set. +func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserToOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.RedirectBrowserTo, true +} + +// SetRedirectBrowserTo sets field value +func (o *ContinueWithRedirectBrowserTo) SetRedirectBrowserTo(v string) { + o.RedirectBrowserTo = v +} + +func (o ContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if true { + toSerialize["action"] = o.Action + } + if true { + toSerialize["redirect_browser_to"] = o.RedirectBrowserTo + } + return json.Marshal(toSerialize) +} + +type NullableContinueWithRedirectBrowserTo struct { + value *ContinueWithRedirectBrowserTo + isSet bool +} + +func (v NullableContinueWithRedirectBrowserTo) Get() *ContinueWithRedirectBrowserTo { + return v.value +} + +func (v *NullableContinueWithRedirectBrowserTo) Set(val *ContinueWithRedirectBrowserTo) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithRedirectBrowserTo) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithRedirectBrowserTo) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithRedirectBrowserTo(val *ContinueWithRedirectBrowserTo) *NullableContinueWithRedirectBrowserTo { + return &NullableContinueWithRedirectBrowserTo{value: val, isSet: true} +} + +func (v NullableContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithRedirectBrowserTo) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_continue_with_settings_ui_flow.go b/internal/client-go/model_continue_with_settings_ui_flow.go index 4ccaf74ef1b8..d6e9b9441f99 100644 --- a/internal/client-go/model_continue_with_settings_ui_flow.go +++ b/internal/client-go/model_continue_with_settings_ui_flow.go @@ -19,6 +19,8 @@ import ( type ContinueWithSettingsUiFlow struct { // The ID of the settings flow Id string `json:"id"` + // The URL of the settings flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. + Url *string `json:"url,omitempty"` } // NewContinueWithSettingsUiFlow instantiates a new ContinueWithSettingsUiFlow object @@ -63,11 +65,46 @@ func (o *ContinueWithSettingsUiFlow) SetId(v string) { o.Id = v } +// GetUrl returns the Url field value if set, zero value otherwise. +func (o *ContinueWithSettingsUiFlow) GetUrl() string { + if o == nil || o.Url == nil { + var ret string + return ret + } + return *o.Url +} + +// GetUrlOk returns a tuple with the Url field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ContinueWithSettingsUiFlow) GetUrlOk() (*string, bool) { + if o == nil || o.Url == nil { + return nil, false + } + return o.Url, true +} + +// HasUrl returns a boolean if a field has been set. +func (o *ContinueWithSettingsUiFlow) HasUrl() bool { + if o != nil && o.Url != nil { + return true + } + + return false +} + +// SetUrl gets a reference to the given string and assigns it to the Url field. +func (o *ContinueWithSettingsUiFlow) SetUrl(v string) { + o.Url = &v +} + func (o ContinueWithSettingsUiFlow) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if true { toSerialize["id"] = o.Id } + if o.Url != nil { + toSerialize["url"] = o.Url + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_continue_with_verification_ui_flow.go b/internal/client-go/model_continue_with_verification_ui_flow.go index 8fdd4609cf93..3c73a0761339 100644 --- a/internal/client-go/model_continue_with_verification_ui_flow.go +++ b/internal/client-go/model_continue_with_verification_ui_flow.go @@ -19,7 +19,7 @@ import ( type ContinueWithVerificationUiFlow struct { // The ID of the verification flow Id string `json:"id"` - // The URL of the verification flow + // The URL of the verification flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. Url *string `json:"url,omitempty"` // The address that should be verified in this flow VerifiableAddress string `json:"verifiable_address"` diff --git a/internal/client-go/model_create_recovery_code_for_identity_body.go b/internal/client-go/model_create_recovery_code_for_identity_body.go index 850c7e086206..2947fad34e51 100644 --- a/internal/client-go/model_create_recovery_code_for_identity_body.go +++ b/internal/client-go/model_create_recovery_code_for_identity_body.go @@ -19,6 +19,8 @@ import ( type CreateRecoveryCodeForIdentityBody struct { // Code Expires In The recovery code will expire after that amount of time has passed. Defaults to the configuration value of `selfservice.methods.code.config.lifespan`. ExpiresIn *string `json:"expires_in,omitempty"` + // The flow type can either be `api` or `browser`. + FlowType *string `json:"flow_type,omitempty"` // Identity to Recover The identity's ID you wish to recover. IdentityId string `json:"identity_id"` } @@ -73,6 +75,38 @@ func (o *CreateRecoveryCodeForIdentityBody) SetExpiresIn(v string) { o.ExpiresIn = &v } +// GetFlowType returns the FlowType field value if set, zero value otherwise. +func (o *CreateRecoveryCodeForIdentityBody) GetFlowType() string { + if o == nil || o.FlowType == nil { + var ret string + return ret + } + return *o.FlowType +} + +// GetFlowTypeOk returns a tuple with the FlowType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateRecoveryCodeForIdentityBody) GetFlowTypeOk() (*string, bool) { + if o == nil || o.FlowType == nil { + return nil, false + } + return o.FlowType, true +} + +// HasFlowType returns a boolean if a field has been set. +func (o *CreateRecoveryCodeForIdentityBody) HasFlowType() bool { + if o != nil && o.FlowType != nil { + return true + } + + return false +} + +// SetFlowType gets a reference to the given string and assigns it to the FlowType field. +func (o *CreateRecoveryCodeForIdentityBody) SetFlowType(v string) { + o.FlowType = &v +} + // GetIdentityId returns the IdentityId field value func (o *CreateRecoveryCodeForIdentityBody) GetIdentityId() string { if o == nil { @@ -102,6 +136,9 @@ func (o CreateRecoveryCodeForIdentityBody) MarshalJSON() ([]byte, error) { if o.ExpiresIn != nil { toSerialize["expires_in"] = o.ExpiresIn } + if o.FlowType != nil { + toSerialize["flow_type"] = o.FlowType + } if true { toSerialize["identity_id"] = o.IdentityId } diff --git a/internal/client-go/model_identity_credentials.go b/internal/client-go/model_identity_credentials.go index 5b454383d520..7ee96800df4b 100644 --- a/internal/client-go/model_identity_credentials.go +++ b/internal/client-go/model_identity_credentials.go @@ -23,7 +23,7 @@ type IdentityCredentials struct { CreatedAt *time.Time `json:"created_at,omitempty"` // Identifiers represents a list of unique identifiers this credential type matches. Identifiers []string `json:"identifiers,omitempty"` - // Type discriminates between different types of credentials. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + // Type discriminates between different types of credentials. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode Type *string `json:"type,omitempty"` // UpdatedAt is a helper struct field for gobuffalo.pop. UpdatedAt *time.Time `json:"updated_at,omitempty"` diff --git a/internal/client-go/model_identity_credentials_code.go b/internal/client-go/model_identity_credentials_code.go index 75857f31c272..53fefb6719eb 100644 --- a/internal/client-go/model_identity_credentials_code.go +++ b/internal/client-go/model_identity_credentials_code.go @@ -13,14 +13,11 @@ package client import ( "encoding/json" - "time" ) // IdentityCredentialsCode CredentialsCode represents a one time login/registration code type IdentityCredentialsCode struct { - // The type of the address for this code - AddressType *string `json:"address_type,omitempty"` - UsedAt NullableTime `json:"used_at,omitempty"` + Addresses []IdentityCredentialsCodeAddress `json:"addresses,omitempty"` } // NewIdentityCredentialsCode instantiates a new IdentityCredentialsCode object @@ -40,88 +37,42 @@ func NewIdentityCredentialsCodeWithDefaults() *IdentityCredentialsCode { return &this } -// GetAddressType returns the AddressType field value if set, zero value otherwise. -func (o *IdentityCredentialsCode) GetAddressType() string { - if o == nil || o.AddressType == nil { - var ret string +// GetAddresses returns the Addresses field value if set, zero value otherwise. +func (o *IdentityCredentialsCode) GetAddresses() []IdentityCredentialsCodeAddress { + if o == nil || o.Addresses == nil { + var ret []IdentityCredentialsCodeAddress return ret } - return *o.AddressType + return o.Addresses } -// GetAddressTypeOk returns a tuple with the AddressType field value if set, nil otherwise +// GetAddressesOk returns a tuple with the Addresses field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *IdentityCredentialsCode) GetAddressTypeOk() (*string, bool) { - if o == nil || o.AddressType == nil { +func (o *IdentityCredentialsCode) GetAddressesOk() ([]IdentityCredentialsCodeAddress, bool) { + if o == nil || o.Addresses == nil { return nil, false } - return o.AddressType, true + return o.Addresses, true } -// HasAddressType returns a boolean if a field has been set. -func (o *IdentityCredentialsCode) HasAddressType() bool { - if o != nil && o.AddressType != nil { +// HasAddresses returns a boolean if a field has been set. +func (o *IdentityCredentialsCode) HasAddresses() bool { + if o != nil && o.Addresses != nil { return true } return false } -// SetAddressType gets a reference to the given string and assigns it to the AddressType field. -func (o *IdentityCredentialsCode) SetAddressType(v string) { - o.AddressType = &v -} - -// GetUsedAt returns the UsedAt field value if set, zero value otherwise (both if not set or set to explicit null). -func (o *IdentityCredentialsCode) GetUsedAt() time.Time { - if o == nil || o.UsedAt.Get() == nil { - var ret time.Time - return ret - } - return *o.UsedAt.Get() -} - -// GetUsedAtOk returns a tuple with the UsedAt field value if set, nil otherwise -// and a boolean to check if the value has been set. -// NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *IdentityCredentialsCode) GetUsedAtOk() (*time.Time, bool) { - if o == nil { - return nil, false - } - return o.UsedAt.Get(), o.UsedAt.IsSet() -} - -// HasUsedAt returns a boolean if a field has been set. -func (o *IdentityCredentialsCode) HasUsedAt() bool { - if o != nil && o.UsedAt.IsSet() { - return true - } - - return false -} - -// SetUsedAt gets a reference to the given NullableTime and assigns it to the UsedAt field. -func (o *IdentityCredentialsCode) SetUsedAt(v time.Time) { - o.UsedAt.Set(&v) -} - -// SetUsedAtNil sets the value for UsedAt to be an explicit nil -func (o *IdentityCredentialsCode) SetUsedAtNil() { - o.UsedAt.Set(nil) -} - -// UnsetUsedAt ensures that no value is present for UsedAt, not even an explicit nil -func (o *IdentityCredentialsCode) UnsetUsedAt() { - o.UsedAt.Unset() +// SetAddresses gets a reference to the given []IdentityCredentialsCodeAddress and assigns it to the Addresses field. +func (o *IdentityCredentialsCode) SetAddresses(v []IdentityCredentialsCodeAddress) { + o.Addresses = v } func (o IdentityCredentialsCode) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} - if o.AddressType != nil { - toSerialize["address_type"] = o.AddressType - } - if o.UsedAt.IsSet() { - toSerialize["used_at"] = o.UsedAt.Get() + if o.Addresses != nil { + toSerialize["addresses"] = o.Addresses } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_identity_credentials_code_address.go b/internal/client-go/model_identity_credentials_code_address.go new file mode 100644 index 000000000000..c739045e79e0 --- /dev/null +++ b/internal/client-go/model_identity_credentials_code_address.go @@ -0,0 +1,151 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// IdentityCredentialsCodeAddress struct for IdentityCredentialsCodeAddress +type IdentityCredentialsCodeAddress struct { + // The address for this code + Address *string `json:"address,omitempty"` + Channel *string `json:"channel,omitempty"` +} + +// NewIdentityCredentialsCodeAddress instantiates a new IdentityCredentialsCodeAddress object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewIdentityCredentialsCodeAddress() *IdentityCredentialsCodeAddress { + this := IdentityCredentialsCodeAddress{} + return &this +} + +// NewIdentityCredentialsCodeAddressWithDefaults instantiates a new IdentityCredentialsCodeAddress object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewIdentityCredentialsCodeAddressWithDefaults() *IdentityCredentialsCodeAddress { + this := IdentityCredentialsCodeAddress{} + return &this +} + +// GetAddress returns the Address field value if set, zero value otherwise. +func (o *IdentityCredentialsCodeAddress) GetAddress() string { + if o == nil || o.Address == nil { + var ret string + return ret + } + return *o.Address +} + +// GetAddressOk returns a tuple with the Address field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsCodeAddress) GetAddressOk() (*string, bool) { + if o == nil || o.Address == nil { + return nil, false + } + return o.Address, true +} + +// HasAddress returns a boolean if a field has been set. +func (o *IdentityCredentialsCodeAddress) HasAddress() bool { + if o != nil && o.Address != nil { + return true + } + + return false +} + +// SetAddress gets a reference to the given string and assigns it to the Address field. +func (o *IdentityCredentialsCodeAddress) SetAddress(v string) { + o.Address = &v +} + +// GetChannel returns the Channel field value if set, zero value otherwise. +func (o *IdentityCredentialsCodeAddress) GetChannel() string { + if o == nil || o.Channel == nil { + var ret string + return ret + } + return *o.Channel +} + +// GetChannelOk returns a tuple with the Channel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsCodeAddress) GetChannelOk() (*string, bool) { + if o == nil || o.Channel == nil { + return nil, false + } + return o.Channel, true +} + +// HasChannel returns a boolean if a field has been set. +func (o *IdentityCredentialsCodeAddress) HasChannel() bool { + if o != nil && o.Channel != nil { + return true + } + + return false +} + +// SetChannel gets a reference to the given string and assigns it to the Channel field. +func (o *IdentityCredentialsCodeAddress) SetChannel(v string) { + o.Channel = &v +} + +func (o IdentityCredentialsCodeAddress) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Address != nil { + toSerialize["address"] = o.Address + } + if o.Channel != nil { + toSerialize["channel"] = o.Channel + } + return json.Marshal(toSerialize) +} + +type NullableIdentityCredentialsCodeAddress struct { + value *IdentityCredentialsCodeAddress + isSet bool +} + +func (v NullableIdentityCredentialsCodeAddress) Get() *IdentityCredentialsCodeAddress { + return v.value +} + +func (v *NullableIdentityCredentialsCodeAddress) Set(val *IdentityCredentialsCodeAddress) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsCodeAddress) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsCodeAddress) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsCodeAddress(val *IdentityCredentialsCodeAddress) *NullableIdentityCredentialsCodeAddress { + return &NullableIdentityCredentialsCodeAddress{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsCodeAddress) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsCodeAddress) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_identity_credentials_password.go b/internal/client-go/model_identity_credentials_password.go index 85f942fb6852..df1900568bb3 100644 --- a/internal/client-go/model_identity_credentials_password.go +++ b/internal/client-go/model_identity_credentials_password.go @@ -19,6 +19,8 @@ import ( type IdentityCredentialsPassword struct { // HashedPassword is a hash-representation of the password. HashedPassword *string `json:"hashed_password,omitempty"` + // UsePasswordMigrationHook is set to true if the password should be migrated using the password migration hook. If set, and the HashedPassword is empty, a webhook will be called during login to migrate the password. + UsePasswordMigrationHook *bool `json:"use_password_migration_hook,omitempty"` } // NewIdentityCredentialsPassword instantiates a new IdentityCredentialsPassword object @@ -70,11 +72,46 @@ func (o *IdentityCredentialsPassword) SetHashedPassword(v string) { o.HashedPassword = &v } +// GetUsePasswordMigrationHook returns the UsePasswordMigrationHook field value if set, zero value otherwise. +func (o *IdentityCredentialsPassword) GetUsePasswordMigrationHook() bool { + if o == nil || o.UsePasswordMigrationHook == nil { + var ret bool + return ret + } + return *o.UsePasswordMigrationHook +} + +// GetUsePasswordMigrationHookOk returns a tuple with the UsePasswordMigrationHook field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsPassword) GetUsePasswordMigrationHookOk() (*bool, bool) { + if o == nil || o.UsePasswordMigrationHook == nil { + return nil, false + } + return o.UsePasswordMigrationHook, true +} + +// HasUsePasswordMigrationHook returns a boolean if a field has been set. +func (o *IdentityCredentialsPassword) HasUsePasswordMigrationHook() bool { + if o != nil && o.UsePasswordMigrationHook != nil { + return true + } + + return false +} + +// SetUsePasswordMigrationHook gets a reference to the given bool and assigns it to the UsePasswordMigrationHook field. +func (o *IdentityCredentialsPassword) SetUsePasswordMigrationHook(v bool) { + o.UsePasswordMigrationHook = &v +} + func (o IdentityCredentialsPassword) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.HashedPassword != nil { toSerialize["hashed_password"] = o.HashedPassword } + if o.UsePasswordMigrationHook != nil { + toSerialize["use_password_migration_hook"] = o.UsePasswordMigrationHook + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_identity_patch_response.go b/internal/client-go/model_identity_patch_response.go index 2ee305f7da81..f67224edad01 100644 --- a/internal/client-go/model_identity_patch_response.go +++ b/internal/client-go/model_identity_patch_response.go @@ -17,8 +17,9 @@ import ( // IdentityPatchResponse Response for a single identity patch type IdentityPatchResponse struct { - // The action for this specific patch create ActionCreate Create this identity. - Action *string `json:"action,omitempty"` + // The action for this specific patch create ActionCreate Create this identity. error ActionError Error indicates that the patch failed. + Action *string `json:"action,omitempty"` + Error interface{} `json:"error,omitempty"` // The identity ID payload of this patch Identity *string `json:"identity,omitempty"` // The ID of this patch response, if an ID was specified in the patch. @@ -74,6 +75,39 @@ func (o *IdentityPatchResponse) SetAction(v string) { o.Action = &v } +// GetError returns the Error field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *IdentityPatchResponse) GetError() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.Error +} + +// GetErrorOk returns a tuple with the Error field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *IdentityPatchResponse) GetErrorOk() (*interface{}, bool) { + if o == nil || o.Error == nil { + return nil, false + } + return &o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *IdentityPatchResponse) HasError() bool { + if o != nil && o.Error != nil { + return true + } + + return false +} + +// SetError gets a reference to the given interface{} and assigns it to the Error field. +func (o *IdentityPatchResponse) SetError(v interface{}) { + o.Error = v +} + // GetIdentity returns the Identity field value if set, zero value otherwise. func (o *IdentityPatchResponse) GetIdentity() string { if o == nil || o.Identity == nil { @@ -143,6 +177,9 @@ func (o IdentityPatchResponse) MarshalJSON() ([]byte, error) { if o.Action != nil { toSerialize["action"] = o.Action } + if o.Error != nil { + toSerialize["error"] = o.Error + } if o.Identity != nil { toSerialize["identity"] = o.Identity } diff --git a/internal/client-go/model_identity_with_credentials_password_config.go b/internal/client-go/model_identity_with_credentials_password_config.go index 754d59460f83..34f09ae58232 100644 --- a/internal/client-go/model_identity_with_credentials_password_config.go +++ b/internal/client-go/model_identity_with_credentials_password_config.go @@ -21,6 +21,8 @@ type IdentityWithCredentialsPasswordConfig struct { HashedPassword *string `json:"hashed_password,omitempty"` // The password in plain text if no hash is available. Password *string `json:"password,omitempty"` + // If set to true, the password will be migrated using the password migration hook. + UsePasswordMigrationHook *bool `json:"use_password_migration_hook,omitempty"` } // NewIdentityWithCredentialsPasswordConfig instantiates a new IdentityWithCredentialsPasswordConfig object @@ -104,6 +106,38 @@ func (o *IdentityWithCredentialsPasswordConfig) SetPassword(v string) { o.Password = &v } +// GetUsePasswordMigrationHook returns the UsePasswordMigrationHook field value if set, zero value otherwise. +func (o *IdentityWithCredentialsPasswordConfig) GetUsePasswordMigrationHook() bool { + if o == nil || o.UsePasswordMigrationHook == nil { + var ret bool + return ret + } + return *o.UsePasswordMigrationHook +} + +// GetUsePasswordMigrationHookOk returns a tuple with the UsePasswordMigrationHook field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsPasswordConfig) GetUsePasswordMigrationHookOk() (*bool, bool) { + if o == nil || o.UsePasswordMigrationHook == nil { + return nil, false + } + return o.UsePasswordMigrationHook, true +} + +// HasUsePasswordMigrationHook returns a boolean if a field has been set. +func (o *IdentityWithCredentialsPasswordConfig) HasUsePasswordMigrationHook() bool { + if o != nil && o.UsePasswordMigrationHook != nil { + return true + } + + return false +} + +// SetUsePasswordMigrationHook gets a reference to the given bool and assigns it to the UsePasswordMigrationHook field. +func (o *IdentityWithCredentialsPasswordConfig) SetUsePasswordMigrationHook(v bool) { + o.UsePasswordMigrationHook = &v +} + func (o IdentityWithCredentialsPasswordConfig) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.HashedPassword != nil { @@ -112,6 +146,9 @@ func (o IdentityWithCredentialsPasswordConfig) MarshalJSON() ([]byte, error) { if o.Password != nil { toSerialize["password"] = o.Password } + if o.UsePasswordMigrationHook != nil { + toSerialize["use_password_migration_hook"] = o.UsePasswordMigrationHook + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_login_flow.go b/internal/client-go/model_login_flow.go index b36abc379568..2794adee0b83 100644 --- a/internal/client-go/model_login_flow.go +++ b/internal/client-go/model_login_flow.go @@ -18,7 +18,7 @@ import ( // LoginFlow This object represents a login flow. A login flow is initiated at the \"Initiate Login API / Browser Flow\" endpoint by a client. Once a login flow is completed successfully, a session cookie or session token will be issued. type LoginFlow struct { - // The active login method If set contains the login method used. If the flow is new, it is unset. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + // The active login method If set contains the login method used. If the flow is new, it is unset. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode Active *string `json:"active,omitempty"` // CreatedAt is a helper struct field for gobuffalo.pop. CreatedAt *time.Time `json:"created_at,omitempty"` @@ -43,6 +43,8 @@ type LoginFlow struct { SessionTokenExchangeCode *string `json:"session_token_exchange_code,omitempty"` // State represents the state of this request: choose_method: ask the user to choose a method to sign in with sent_email: the email has been sent to the user passed_challenge: the request was successful and the login challenge was passed. State interface{} `json:"state"` + // TransientPayload is used to pass data from the login to hooks and email templates + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -495,6 +497,38 @@ func (o *LoginFlow) SetState(v interface{}) { o.State = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *LoginFlow) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *LoginFlow) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *LoginFlow) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetType returns the Type field value func (o *LoginFlow) GetType() string { if o == nil { @@ -619,6 +653,9 @@ func (o LoginFlow) MarshalJSON() ([]byte, error) { if o.State != nil { toSerialize["state"] = o.State } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if true { toSerialize["type"] = o.Type } diff --git a/internal/client-go/model_login_flow_state.go b/internal/client-go/model_login_flow_state.go index ce5570b79032..58af057c612f 100644 --- a/internal/client-go/model_login_flow_state.go +++ b/internal/client-go/model_login_flow_state.go @@ -16,7 +16,7 @@ import ( "fmt" ) -// LoginFlowState The state represents the state of the login flow. choose_method: ask the user to choose a method (e.g. login account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the login challenge was passed. +// LoginFlowState The experimental state represents the state of a login flow. This field is EXPERIMENTAL and subject to change! type LoginFlowState string // List of loginFlowState diff --git a/internal/client-go/model_o_auth2_client.go b/internal/client-go/model_o_auth2_client.go index be48d3217ade..bbf75b334104 100644 --- a/internal/client-go/model_o_auth2_client.go +++ b/internal/client-go/model_o_auth2_client.go @@ -18,6 +18,7 @@ import ( // OAuth2Client struct for OAuth2Client type OAuth2Client struct { + AdditionalPropertiesField map[string]interface{} `json:"AdditionalProperties,omitempty"` // OAuth 2.0 Access Token Strategy AccessTokenStrategy is the strategy used to generate access tokens. Valid options are `jwt` and `opaque`. `jwt` is a bad idea, see https://www.ory.sh/docs/hydra/advanced#json-web-tokens Setting the stragegy here overrides the global setting in `strategies.access_token`. AccessTokenStrategy *string `json:"access_token_strategy,omitempty"` AllowedCorsOrigins []string `json:"allowed_cors_origins,omitempty"` @@ -124,6 +125,38 @@ func NewOAuth2ClientWithDefaults() *OAuth2Client { return &this } +// GetAdditionalPropertiesField returns the AdditionalPropertiesField field value if set, zero value otherwise. +func (o *OAuth2Client) GetAdditionalPropertiesField() map[string]interface{} { + if o == nil || o.AdditionalPropertiesField == nil { + var ret map[string]interface{} + return ret + } + return o.AdditionalPropertiesField +} + +// GetAdditionalPropertiesFieldOk returns a tuple with the AdditionalPropertiesField field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetAdditionalPropertiesFieldOk() (map[string]interface{}, bool) { + if o == nil || o.AdditionalPropertiesField == nil { + return nil, false + } + return o.AdditionalPropertiesField, true +} + +// HasAdditionalPropertiesField returns a boolean if a field has been set. +func (o *OAuth2Client) HasAdditionalPropertiesField() bool { + if o != nil && o.AdditionalPropertiesField != nil { + return true + } + + return false +} + +// SetAdditionalPropertiesField gets a reference to the given map[string]interface{} and assigns it to the AdditionalPropertiesField field. +func (o *OAuth2Client) SetAdditionalPropertiesField(v map[string]interface{}) { + o.AdditionalPropertiesField = v +} + // GetAccessTokenStrategy returns the AccessTokenStrategy field value if set, zero value otherwise. func (o *OAuth2Client) GetAccessTokenStrategy() string { if o == nil || o.AccessTokenStrategy == nil { @@ -1664,6 +1697,9 @@ func (o *OAuth2Client) SetUserinfoSignedResponseAlg(v string) { func (o OAuth2Client) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} + if o.AdditionalPropertiesField != nil { + toSerialize["AdditionalProperties"] = o.AdditionalPropertiesField + } if o.AccessTokenStrategy != nil { toSerialize["access_token_strategy"] = o.AccessTokenStrategy } diff --git a/internal/client-go/model_o_auth2_consent_request_open_id_connect_context.go b/internal/client-go/model_o_auth2_consent_request_open_id_connect_context.go index c0cbf7f3129e..68884830a9ee 100644 --- a/internal/client-go/model_o_auth2_consent_request_open_id_connect_context.go +++ b/internal/client-go/model_o_auth2_consent_request_open_id_connect_context.go @@ -17,6 +17,7 @@ import ( // OAuth2ConsentRequestOpenIDConnectContext OAuth2ConsentRequestOpenIDConnectContext struct for OAuth2ConsentRequestOpenIDConnectContext type OAuth2ConsentRequestOpenIDConnectContext struct { + AdditionalPropertiesField map[string]interface{} `json:"AdditionalProperties,omitempty"` // ACRValues is the Authentication AuthorizationContext Class Reference requested in the OAuth 2.0 Authorization request. It is a parameter defined by OpenID Connect and expresses which level of authentication (e.g. 2FA) is required. OpenID Connect defines it as follows: > Requested Authentication AuthorizationContext Class Reference values. Space-separated string that specifies the acr values that the Authorization Server is being requested to use for processing this Authentication Request, with the values appearing in order of preference. The Authentication AuthorizationContext Class satisfied by the authentication performed is returned as the acr Claim Value, as specified in Section 2. The acr Claim is requested as a Voluntary Claim by this parameter. AcrValues []string `json:"acr_values,omitempty"` // Display is a string value that specifies how the Authorization Server displays the authentication and consent user interface pages to the End-User. The defined values are: page: The Authorization Server SHOULD display the authentication and consent UI consistent with a full User Agent page view. If the display parameter is not specified, this is the default display mode. popup: The Authorization Server SHOULD display the authentication and consent UI consistent with a popup User Agent window. The popup User Agent window should be of an appropriate size for a login-focused dialog and should not obscure the entire window that it is popping up over. touch: The Authorization Server SHOULD display the authentication and consent UI consistent with a device that leverages a touch interface. wap: The Authorization Server SHOULD display the authentication and consent UI consistent with a \\\"feature phone\\\" type display. The Authorization Server MAY also attempt to detect the capabilities of the User Agent and present an appropriate display. @@ -46,6 +47,38 @@ func NewOAuth2ConsentRequestOpenIDConnectContextWithDefaults() *OAuth2ConsentReq return &this } +// GetAdditionalPropertiesField returns the AdditionalPropertiesField field value if set, zero value otherwise. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetAdditionalPropertiesField() map[string]interface{} { + if o == nil || o.AdditionalPropertiesField == nil { + var ret map[string]interface{} + return ret + } + return o.AdditionalPropertiesField +} + +// GetAdditionalPropertiesFieldOk returns a tuple with the AdditionalPropertiesField field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetAdditionalPropertiesFieldOk() (map[string]interface{}, bool) { + if o == nil || o.AdditionalPropertiesField == nil { + return nil, false + } + return o.AdditionalPropertiesField, true +} + +// HasAdditionalPropertiesField returns a boolean if a field has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) HasAdditionalPropertiesField() bool { + if o != nil && o.AdditionalPropertiesField != nil { + return true + } + + return false +} + +// SetAdditionalPropertiesField gets a reference to the given map[string]interface{} and assigns it to the AdditionalPropertiesField field. +func (o *OAuth2ConsentRequestOpenIDConnectContext) SetAdditionalPropertiesField(v map[string]interface{}) { + o.AdditionalPropertiesField = v +} + // GetAcrValues returns the AcrValues field value if set, zero value otherwise. func (o *OAuth2ConsentRequestOpenIDConnectContext) GetAcrValues() []string { if o == nil || o.AcrValues == nil { @@ -208,6 +241,9 @@ func (o *OAuth2ConsentRequestOpenIDConnectContext) SetUiLocales(v []string) { func (o OAuth2ConsentRequestOpenIDConnectContext) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} + if o.AdditionalPropertiesField != nil { + toSerialize["AdditionalProperties"] = o.AdditionalPropertiesField + } if o.AcrValues != nil { toSerialize["acr_values"] = o.AcrValues } diff --git a/internal/client-go/model_o_auth2_login_request.go b/internal/client-go/model_o_auth2_login_request.go index 9fcd87be72fa..a788c66ed3ca 100644 --- a/internal/client-go/model_o_auth2_login_request.go +++ b/internal/client-go/model_o_auth2_login_request.go @@ -17,6 +17,7 @@ import ( // OAuth2LoginRequest OAuth2LoginRequest struct for OAuth2LoginRequest type OAuth2LoginRequest struct { + AdditionalPropertiesField map[string]interface{} `json:"AdditionalProperties,omitempty"` // ID is the identifier (\\\"login challenge\\\") of the login request. It is used to identify the session. Challenge *string `json:"challenge,omitempty"` Client *OAuth2Client `json:"client,omitempty"` @@ -50,6 +51,38 @@ func NewOAuth2LoginRequestWithDefaults() *OAuth2LoginRequest { return &this } +// GetAdditionalPropertiesField returns the AdditionalPropertiesField field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetAdditionalPropertiesField() map[string]interface{} { + if o == nil || o.AdditionalPropertiesField == nil { + var ret map[string]interface{} + return ret + } + return o.AdditionalPropertiesField +} + +// GetAdditionalPropertiesFieldOk returns a tuple with the AdditionalPropertiesField field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetAdditionalPropertiesFieldOk() (map[string]interface{}, bool) { + if o == nil || o.AdditionalPropertiesField == nil { + return nil, false + } + return o.AdditionalPropertiesField, true +} + +// HasAdditionalPropertiesField returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasAdditionalPropertiesField() bool { + if o != nil && o.AdditionalPropertiesField != nil { + return true + } + + return false +} + +// SetAdditionalPropertiesField gets a reference to the given map[string]interface{} and assigns it to the AdditionalPropertiesField field. +func (o *OAuth2LoginRequest) SetAdditionalPropertiesField(v map[string]interface{}) { + o.AdditionalPropertiesField = v +} + // GetChallenge returns the Challenge field value if set, zero value otherwise. func (o *OAuth2LoginRequest) GetChallenge() string { if o == nil || o.Challenge == nil { @@ -340,6 +373,9 @@ func (o *OAuth2LoginRequest) SetSubject(v string) { func (o OAuth2LoginRequest) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} + if o.AdditionalPropertiesField != nil { + toSerialize["AdditionalProperties"] = o.AdditionalPropertiesField + } if o.Challenge != nil { toSerialize["challenge"] = o.Challenge } diff --git a/internal/client-go/model_recovery_flow.go b/internal/client-go/model_recovery_flow.go index e6df63a7c6b2..56f27a904be1 100644 --- a/internal/client-go/model_recovery_flow.go +++ b/internal/client-go/model_recovery_flow.go @@ -34,6 +34,8 @@ type RecoveryFlow struct { ReturnTo *string `json:"return_to,omitempty"` // State represents the state of this request: choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. State interface{} `json:"state"` + // TransientPayload is used to pass data from the recovery flow to hooks and email templates + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -281,6 +283,38 @@ func (o *RecoveryFlow) SetState(v interface{}) { o.State = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *RecoveryFlow) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *RecoveryFlow) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *RecoveryFlow) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetType returns the Type field value func (o *RecoveryFlow) GetType() string { if o == nil { @@ -355,6 +389,9 @@ func (o RecoveryFlow) MarshalJSON() ([]byte, error) { if o.State != nil { toSerialize["state"] = o.State } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if true { toSerialize["type"] = o.Type } diff --git a/internal/client-go/model_recovery_flow_state.go b/internal/client-go/model_recovery_flow_state.go index 1c660ba043b9..d1fa3618882a 100644 --- a/internal/client-go/model_recovery_flow_state.go +++ b/internal/client-go/model_recovery_flow_state.go @@ -16,7 +16,7 @@ import ( "fmt" ) -// RecoveryFlowState The state represents the state of the recovery flow. choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. +// RecoveryFlowState The experimental state represents the state of a recovery flow. This field is EXPERIMENTAL and subject to change! type RecoveryFlowState string // List of recoveryFlowState diff --git a/internal/client-go/model_registration_flow.go b/internal/client-go/model_registration_flow.go index 71ab2c03ebe2..c0ba64843d3f 100644 --- a/internal/client-go/model_registration_flow.go +++ b/internal/client-go/model_registration_flow.go @@ -18,7 +18,7 @@ import ( // RegistrationFlow struct for RegistrationFlow type RegistrationFlow struct { - // Active, if set, contains the registration method that is being used. It is initially not set. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + // Active, if set, contains the registration method that is being used. It is initially not set. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode Active *string `json:"active,omitempty"` // ExpiresAt is the time (UTC) when the flow expires. If the user still wishes to log in, a new flow has to be initiated. ExpiresAt time.Time `json:"expires_at"` diff --git a/internal/client-go/model_registration_flow_state.go b/internal/client-go/model_registration_flow_state.go index 86f3fd38cff0..15fd9f532d4b 100644 --- a/internal/client-go/model_registration_flow_state.go +++ b/internal/client-go/model_registration_flow_state.go @@ -16,7 +16,7 @@ import ( "fmt" ) -// RegistrationFlowState choose_method: ask the user to choose a method (e.g. registration with email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the registration challenge was passed. +// RegistrationFlowState The experimental state represents the state of a registration flow. This field is EXPERIMENTAL and subject to change! type RegistrationFlowState string // List of registrationFlowState diff --git a/internal/client-go/model_settings_flow.go b/internal/client-go/model_settings_flow.go index fa5cd9317c54..f45c1599e8dd 100644 --- a/internal/client-go/model_settings_flow.go +++ b/internal/client-go/model_settings_flow.go @@ -35,6 +35,8 @@ type SettingsFlow struct { ReturnTo *string `json:"return_to,omitempty"` // State represents the state of this flow. It knows two states: show_form: No user data has been collected, or it is invalid, and thus the form should be shown. success: Indicates that the settings flow has been updated successfully with the provided data. Done will stay true when repeatedly checking. If set to true, done will revert back to false only when a flow with invalid (e.g. \"please use a valid phone number\") data was sent. State interface{} `json:"state"` + // TransientPayload is used to pass data from the settings flow to hooks and email templates + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -307,6 +309,38 @@ func (o *SettingsFlow) SetState(v interface{}) { o.State = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *SettingsFlow) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *SettingsFlow) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *SettingsFlow) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetType returns the Type field value func (o *SettingsFlow) GetType() string { if o == nil { @@ -384,6 +418,9 @@ func (o SettingsFlow) MarshalJSON() ([]byte, error) { if o.State != nil { toSerialize["state"] = o.State } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if true { toSerialize["type"] = o.Type } diff --git a/internal/client-go/model_settings_flow_state.go b/internal/client-go/model_settings_flow_state.go index f994c786a2d8..70093c9c4a03 100644 --- a/internal/client-go/model_settings_flow_state.go +++ b/internal/client-go/model_settings_flow_state.go @@ -16,7 +16,7 @@ import ( "fmt" ) -// SettingsFlowState show_form: No user data has been collected, or it is invalid, and thus the form should be shown. success: Indicates that the settings flow has been updated successfully with the provided data. Done will stay true when repeatedly checking. If set to true, done will revert back to false only when a flow with invalid (e.g. \"please use a valid phone number\") data was sent. +// SettingsFlowState The experimental state represents the state of a settings flow. This field is EXPERIMENTAL and subject to change! type SettingsFlowState string // List of settingsFlowState diff --git a/internal/client-go/model_successful_native_login.go b/internal/client-go/model_successful_native_login.go index f87739f19d12..faf59ae906e7 100644 --- a/internal/client-go/model_successful_native_login.go +++ b/internal/client-go/model_successful_native_login.go @@ -17,7 +17,9 @@ import ( // SuccessfulNativeLogin The Response for Login Flows via API type SuccessfulNativeLogin struct { - Session Session `json:"session"` + // Contains a list of actions, that could follow this flow It can, for example, this will contain a reference to the verification flow, created as part of the user's registration or the token of the session. + ContinueWith []ContinueWith `json:"continue_with,omitempty"` + Session Session `json:"session"` // The Session Token A session token is equivalent to a session cookie, but it can be sent in the HTTP Authorization Header: Authorization: bearer ${session-token} The session token is only issued for API flows, not for Browser flows! SessionToken *string `json:"session_token,omitempty"` } @@ -40,6 +42,38 @@ func NewSuccessfulNativeLoginWithDefaults() *SuccessfulNativeLogin { return &this } +// GetContinueWith returns the ContinueWith field value if set, zero value otherwise. +func (o *SuccessfulNativeLogin) GetContinueWith() []ContinueWith { + if o == nil || o.ContinueWith == nil { + var ret []ContinueWith + return ret + } + return o.ContinueWith +} + +// GetContinueWithOk returns a tuple with the ContinueWith field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SuccessfulNativeLogin) GetContinueWithOk() ([]ContinueWith, bool) { + if o == nil || o.ContinueWith == nil { + return nil, false + } + return o.ContinueWith, true +} + +// HasContinueWith returns a boolean if a field has been set. +func (o *SuccessfulNativeLogin) HasContinueWith() bool { + if o != nil && o.ContinueWith != nil { + return true + } + + return false +} + +// SetContinueWith gets a reference to the given []ContinueWith and assigns it to the ContinueWith field. +func (o *SuccessfulNativeLogin) SetContinueWith(v []ContinueWith) { + o.ContinueWith = v +} + // GetSession returns the Session field value func (o *SuccessfulNativeLogin) GetSession() Session { if o == nil { @@ -98,6 +132,9 @@ func (o *SuccessfulNativeLogin) SetSessionToken(v string) { func (o SuccessfulNativeLogin) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} + if o.ContinueWith != nil { + toSerialize["continue_with"] = o.ContinueWith + } if true { toSerialize["session"] = o.Session } diff --git a/internal/client-go/model_ui_node.go b/internal/client-go/model_ui_node.go index ca88879a7414..3582d9e85f67 100644 --- a/internal/client-go/model_ui_node.go +++ b/internal/client-go/model_ui_node.go @@ -18,7 +18,7 @@ import ( // UiNode Nodes are represented as HTML elements or their native UI equivalents. For example, a node can be an `` tag, or an `` but also `some plain text`. type UiNode struct { Attributes UiNodeAttributes `json:"attributes"` - // Group specifies which group (e.g. password authenticator) this node belongs to. default DefaultGroup password PasswordGroup oidc OpenIDConnectGroup profile ProfileGroup link LinkGroup code CodeGroup totp TOTPGroup lookup_secret LookupGroup webauthn WebAuthnGroup + // Group specifies which group (e.g. password authenticator) this node belongs to. default DefaultGroup password PasswordGroup oidc OpenIDConnectGroup profile ProfileGroup link LinkGroup code CodeGroup totp TOTPGroup lookup_secret LookupGroup webauthn WebAuthnGroup passkey PasskeyGroup identifier_first IdentifierFirstGroup Group string `json:"group"` Messages []UiText `json:"messages"` Meta UiNodeMeta `json:"meta"` diff --git a/internal/client-go/model_ui_node_anchor_attributes.go b/internal/client-go/model_ui_node_anchor_attributes.go index 15cb492fe8eb..ad2cc992a119 100644 --- a/internal/client-go/model_ui_node_anchor_attributes.go +++ b/internal/client-go/model_ui_node_anchor_attributes.go @@ -21,7 +21,7 @@ type UiNodeAnchorAttributes struct { Href string `json:"href"` // A unique identifier Id string `json:"id"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"a\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"a\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` Title UiText `json:"title"` } diff --git a/internal/client-go/model_ui_node_attributes.go b/internal/client-go/model_ui_node_attributes.go index 347be6f44a72..510dc20f8564 100644 --- a/internal/client-go/model_ui_node_attributes.go +++ b/internal/client-go/model_ui_node_attributes.go @@ -63,86 +63,134 @@ func UiNodeTextAttributesAsUiNodeAttributes(v *UiNodeTextAttributes) UiNodeAttri // Unmarshal JSON data into one of the pointers in the struct func (dst *UiNodeAttributes) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UiNodeAnchorAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeAnchorAttributes) - if err == nil { - jsonUiNodeAnchorAttributes, _ := json.Marshal(dst.UiNodeAnchorAttributes) - if string(jsonUiNodeAnchorAttributes) == "{}" { // empty struct - dst.UiNodeAnchorAttributes = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'a' + if jsonDict["node_type"] == "a" { + // try to unmarshal JSON data into UiNodeAnchorAttributes + err = json.Unmarshal(data, &dst.UiNodeAnchorAttributes) + if err == nil { + return nil // data stored in dst.UiNodeAnchorAttributes, return on the first match } else { - match++ + dst.UiNodeAnchorAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeAnchorAttributes: %s", err.Error()) } - } else { - dst.UiNodeAnchorAttributes = nil } - // try to unmarshal data into UiNodeImageAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeImageAttributes) - if err == nil { - jsonUiNodeImageAttributes, _ := json.Marshal(dst.UiNodeImageAttributes) - if string(jsonUiNodeImageAttributes) == "{}" { // empty struct - dst.UiNodeImageAttributes = nil + // check if the discriminator value is 'img' + if jsonDict["node_type"] == "img" { + // try to unmarshal JSON data into UiNodeImageAttributes + err = json.Unmarshal(data, &dst.UiNodeImageAttributes) + if err == nil { + return nil // data stored in dst.UiNodeImageAttributes, return on the first match } else { - match++ + dst.UiNodeImageAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeImageAttributes: %s", err.Error()) } - } else { - dst.UiNodeImageAttributes = nil } - // try to unmarshal data into UiNodeInputAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeInputAttributes) - if err == nil { - jsonUiNodeInputAttributes, _ := json.Marshal(dst.UiNodeInputAttributes) - if string(jsonUiNodeInputAttributes) == "{}" { // empty struct - dst.UiNodeInputAttributes = nil + // check if the discriminator value is 'input' + if jsonDict["node_type"] == "input" { + // try to unmarshal JSON data into UiNodeInputAttributes + err = json.Unmarshal(data, &dst.UiNodeInputAttributes) + if err == nil { + return nil // data stored in dst.UiNodeInputAttributes, return on the first match } else { - match++ + dst.UiNodeInputAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeInputAttributes: %s", err.Error()) } - } else { - dst.UiNodeInputAttributes = nil } - // try to unmarshal data into UiNodeScriptAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeScriptAttributes) - if err == nil { - jsonUiNodeScriptAttributes, _ := json.Marshal(dst.UiNodeScriptAttributes) - if string(jsonUiNodeScriptAttributes) == "{}" { // empty struct - dst.UiNodeScriptAttributes = nil + // check if the discriminator value is 'script' + if jsonDict["node_type"] == "script" { + // try to unmarshal JSON data into UiNodeScriptAttributes + err = json.Unmarshal(data, &dst.UiNodeScriptAttributes) + if err == nil { + return nil // data stored in dst.UiNodeScriptAttributes, return on the first match } else { - match++ + dst.UiNodeScriptAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeScriptAttributes: %s", err.Error()) } - } else { - dst.UiNodeScriptAttributes = nil } - // try to unmarshal data into UiNodeTextAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeTextAttributes) - if err == nil { - jsonUiNodeTextAttributes, _ := json.Marshal(dst.UiNodeTextAttributes) - if string(jsonUiNodeTextAttributes) == "{}" { // empty struct + // check if the discriminator value is 'text' + if jsonDict["node_type"] == "text" { + // try to unmarshal JSON data into UiNodeTextAttributes + err = json.Unmarshal(data, &dst.UiNodeTextAttributes) + if err == nil { + return nil // data stored in dst.UiNodeTextAttributes, return on the first match + } else { dst.UiNodeTextAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeTextAttributes: %s", err.Error()) + } + } + + // check if the discriminator value is 'uiNodeAnchorAttributes' + if jsonDict["node_type"] == "uiNodeAnchorAttributes" { + // try to unmarshal JSON data into UiNodeAnchorAttributes + err = json.Unmarshal(data, &dst.UiNodeAnchorAttributes) + if err == nil { + return nil // data stored in dst.UiNodeAnchorAttributes, return on the first match + } else { + dst.UiNodeAnchorAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeAnchorAttributes: %s", err.Error()) + } + } + + // check if the discriminator value is 'uiNodeImageAttributes' + if jsonDict["node_type"] == "uiNodeImageAttributes" { + // try to unmarshal JSON data into UiNodeImageAttributes + err = json.Unmarshal(data, &dst.UiNodeImageAttributes) + if err == nil { + return nil // data stored in dst.UiNodeImageAttributes, return on the first match } else { - match++ + dst.UiNodeImageAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeImageAttributes: %s", err.Error()) } - } else { - dst.UiNodeTextAttributes = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UiNodeAnchorAttributes = nil - dst.UiNodeImageAttributes = nil - dst.UiNodeInputAttributes = nil - dst.UiNodeScriptAttributes = nil - dst.UiNodeTextAttributes = nil + // check if the discriminator value is 'uiNodeInputAttributes' + if jsonDict["node_type"] == "uiNodeInputAttributes" { + // try to unmarshal JSON data into UiNodeInputAttributes + err = json.Unmarshal(data, &dst.UiNodeInputAttributes) + if err == nil { + return nil // data stored in dst.UiNodeInputAttributes, return on the first match + } else { + dst.UiNodeInputAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeInputAttributes: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UiNodeAttributes)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UiNodeAttributes)") + // check if the discriminator value is 'uiNodeScriptAttributes' + if jsonDict["node_type"] == "uiNodeScriptAttributes" { + // try to unmarshal JSON data into UiNodeScriptAttributes + err = json.Unmarshal(data, &dst.UiNodeScriptAttributes) + if err == nil { + return nil // data stored in dst.UiNodeScriptAttributes, return on the first match + } else { + dst.UiNodeScriptAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeScriptAttributes: %s", err.Error()) + } + } + + // check if the discriminator value is 'uiNodeTextAttributes' + if jsonDict["node_type"] == "uiNodeTextAttributes" { + // try to unmarshal JSON data into UiNodeTextAttributes + err = json.Unmarshal(data, &dst.UiNodeTextAttributes) + if err == nil { + return nil // data stored in dst.UiNodeTextAttributes, return on the first match + } else { + dst.UiNodeTextAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeTextAttributes: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/client-go/model_ui_node_image_attributes.go b/internal/client-go/model_ui_node_image_attributes.go index 5b300b9548bd..6eef160e3d67 100644 --- a/internal/client-go/model_ui_node_image_attributes.go +++ b/internal/client-go/model_ui_node_image_attributes.go @@ -21,7 +21,7 @@ type UiNodeImageAttributes struct { Height int64 `json:"height"` // A unique identifier Id string `json:"id"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"img\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"img\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` // The image's source URL. format: uri Src string `json:"src"` diff --git a/internal/client-go/model_ui_node_input_attributes.go b/internal/client-go/model_ui_node_input_attributes.go index 3ed9dd56e0c6..2c7ce0e2e5a9 100644 --- a/internal/client-go/model_ui_node_input_attributes.go +++ b/internal/client-go/model_ui_node_input_attributes.go @@ -22,16 +22,22 @@ type UiNodeInputAttributes struct { // Sets the input's disabled field to true or false. Disabled bool `json:"disabled"` Label *UiText `json:"label,omitempty"` - // The maxlength attribute for the input. - Maxlength *int32 `json:"maxlength,omitempty"` + // MaxLength may contain the input's maximum length. + Maxlength *int64 `json:"maxlength,omitempty"` // The minlength attribute for the input. Minlength *int32 `json:"minlength,omitempty"` // The input's element name. Name string `json:"name"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` - // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. + // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. Deprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead. Onclick *string `json:"onclick,omitempty"` + // OnClickTrigger may contain a WebAuthn trigger which should be executed on click. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration + OnclickTrigger *string `json:"onclickTrigger,omitempty"` + // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn. Deprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead. + Onload *string `json:"onload,omitempty"` + // OnLoadTrigger may contain a WebAuthn trigger which should be executed on load. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration + OnloadTrigger *string `json:"onloadTrigger,omitempty"` // The input's pattern. Pattern *string `json:"pattern,omitempty"` // Mark this input field as required. @@ -152,9 +158,9 @@ func (o *UiNodeInputAttributes) SetLabel(v UiText) { } // GetMaxlength returns the Maxlength field value if set, zero value otherwise. -func (o *UiNodeInputAttributes) GetMaxlength() int32 { +func (o *UiNodeInputAttributes) GetMaxlength() int64 { if o == nil || o.Maxlength == nil { - var ret int32 + var ret int64 return ret } return *o.Maxlength @@ -162,7 +168,7 @@ func (o *UiNodeInputAttributes) GetMaxlength() int32 { // GetMaxlengthOk returns a tuple with the Maxlength field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *UiNodeInputAttributes) GetMaxlengthOk() (*int32, bool) { +func (o *UiNodeInputAttributes) GetMaxlengthOk() (*int64, bool) { if o == nil || o.Maxlength == nil { return nil, false } @@ -178,8 +184,8 @@ func (o *UiNodeInputAttributes) HasMaxlength() bool { return false } -// SetMaxlength gets a reference to the given int32 and assigns it to the Maxlength field. -func (o *UiNodeInputAttributes) SetMaxlength(v int32) { +// SetMaxlength gets a reference to the given int64 and assigns it to the Maxlength field. +func (o *UiNodeInputAttributes) SetMaxlength(v int64) { o.Maxlength = &v } @@ -295,6 +301,102 @@ func (o *UiNodeInputAttributes) SetOnclick(v string) { o.Onclick = &v } +// GetOnclickTrigger returns the OnclickTrigger field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOnclickTrigger() string { + if o == nil || o.OnclickTrigger == nil { + var ret string + return ret + } + return *o.OnclickTrigger +} + +// GetOnclickTriggerOk returns a tuple with the OnclickTrigger field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOnclickTriggerOk() (*string, bool) { + if o == nil || o.OnclickTrigger == nil { + return nil, false + } + return o.OnclickTrigger, true +} + +// HasOnclickTrigger returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOnclickTrigger() bool { + if o != nil && o.OnclickTrigger != nil { + return true + } + + return false +} + +// SetOnclickTrigger gets a reference to the given string and assigns it to the OnclickTrigger field. +func (o *UiNodeInputAttributes) SetOnclickTrigger(v string) { + o.OnclickTrigger = &v +} + +// GetOnload returns the Onload field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOnload() string { + if o == nil || o.Onload == nil { + var ret string + return ret + } + return *o.Onload +} + +// GetOnloadOk returns a tuple with the Onload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOnloadOk() (*string, bool) { + if o == nil || o.Onload == nil { + return nil, false + } + return o.Onload, true +} + +// HasOnload returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOnload() bool { + if o != nil && o.Onload != nil { + return true + } + + return false +} + +// SetOnload gets a reference to the given string and assigns it to the Onload field. +func (o *UiNodeInputAttributes) SetOnload(v string) { + o.Onload = &v +} + +// GetOnloadTrigger returns the OnloadTrigger field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOnloadTrigger() string { + if o == nil || o.OnloadTrigger == nil { + var ret string + return ret + } + return *o.OnloadTrigger +} + +// GetOnloadTriggerOk returns a tuple with the OnloadTrigger field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOnloadTriggerOk() (*string, bool) { + if o == nil || o.OnloadTrigger == nil { + return nil, false + } + return o.OnloadTrigger, true +} + +// HasOnloadTrigger returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOnloadTrigger() bool { + if o != nil && o.OnloadTrigger != nil { + return true + } + + return false +} + +// SetOnloadTrigger gets a reference to the given string and assigns it to the OnloadTrigger field. +func (o *UiNodeInputAttributes) SetOnloadTrigger(v string) { + o.OnloadTrigger = &v +} + // GetPattern returns the Pattern field value if set, zero value otherwise. func (o *UiNodeInputAttributes) GetPattern() string { if o == nil || o.Pattern == nil { @@ -442,6 +544,15 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) { if o.Onclick != nil { toSerialize["onclick"] = o.Onclick } + if o.OnclickTrigger != nil { + toSerialize["onclickTrigger"] = o.OnclickTrigger + } + if o.Onload != nil { + toSerialize["onload"] = o.Onload + } + if o.OnloadTrigger != nil { + toSerialize["onloadTrigger"] = o.OnloadTrigger + } if o.Pattern != nil { toSerialize["pattern"] = o.Pattern } diff --git a/internal/client-go/model_ui_node_script_attributes.go b/internal/client-go/model_ui_node_script_attributes.go index 21dca70cbe86..d867c1c66dba 100644 --- a/internal/client-go/model_ui_node_script_attributes.go +++ b/internal/client-go/model_ui_node_script_attributes.go @@ -25,7 +25,7 @@ type UiNodeScriptAttributes struct { Id string `json:"id"` // The script's integrity hash Integrity string `json:"integrity"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"script\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"script\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` // Nonce for CSP A nonce you may want to use to improve your Content Security Policy. You do not have to use this value but if you want to improve your CSP policies you may use it. You can also choose to use your own nonce value! Nonce string `json:"nonce"` diff --git a/internal/client-go/model_ui_node_text_attributes.go b/internal/client-go/model_ui_node_text_attributes.go index 6199187b5d75..93e0f8314191 100644 --- a/internal/client-go/model_ui_node_text_attributes.go +++ b/internal/client-go/model_ui_node_text_attributes.go @@ -19,7 +19,7 @@ import ( type UiNodeTextAttributes struct { // A unique identifier Id string `json:"id"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"text\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"text\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` Text UiText `json:"text"` } diff --git a/internal/client-go/model_update_login_flow_body.go b/internal/client-go/model_update_login_flow_body.go index 36033328e78d..f0d79322c54f 100644 --- a/internal/client-go/model_update_login_flow_body.go +++ b/internal/client-go/model_update_login_flow_body.go @@ -18,12 +18,14 @@ import ( // UpdateLoginFlowBody - struct for UpdateLoginFlowBody type UpdateLoginFlowBody struct { - UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod - UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod - UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod - UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod - UpdateLoginFlowWithTotpMethod *UpdateLoginFlowWithTotpMethod - UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod + UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod + UpdateLoginFlowWithIdentifierFirstMethod *UpdateLoginFlowWithIdentifierFirstMethod + UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod + UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod + UpdateLoginFlowWithPasskeyMethod *UpdateLoginFlowWithPasskeyMethod + UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod + UpdateLoginFlowWithTotpMethod *UpdateLoginFlowWithTotpMethod + UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod } // UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithCodeMethod wrapped in UpdateLoginFlowBody @@ -33,6 +35,13 @@ func UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithCo } } +// UpdateLoginFlowWithIdentifierFirstMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithIdentifierFirstMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithIdentifierFirstMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithIdentifierFirstMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithIdentifierFirstMethod: v, + } +} + // UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithLookupSecretMethod wrapped in UpdateLoginFlowBody func UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithLookupSecretMethod) UpdateLoginFlowBody { return UpdateLoginFlowBody{ @@ -47,6 +56,13 @@ func UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithOi } } +// UpdateLoginFlowWithPasskeyMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithPasskeyMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithPasskeyMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithPasskeyMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithPasskeyMethod: v, + } +} + // UpdateLoginFlowWithPasswordMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithPasswordMethod wrapped in UpdateLoginFlowBody func UpdateLoginFlowWithPasswordMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithPasswordMethod) UpdateLoginFlowBody { return UpdateLoginFlowBody{ @@ -71,100 +87,206 @@ func UpdateLoginFlowWithWebAuthnMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWi // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateLoginFlowWithCodeMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithCodeMethod) - if err == nil { - jsonUpdateLoginFlowWithCodeMethod, _ := json.Marshal(dst.UpdateLoginFlowWithCodeMethod) - if string(jsonUpdateLoginFlowWithCodeMethod) == "{}" { // empty struct + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'code' + if jsonDict["method"] == "code" { + // try to unmarshal JSON data into UpdateLoginFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithCodeMethod, return on the first match + } else { dst.UpdateLoginFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithCodeMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'identifier_first' + if jsonDict["method"] == "identifier_first" { + // try to unmarshal JSON data into UpdateLoginFlowWithIdentifierFirstMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithIdentifierFirstMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithIdentifierFirstMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithIdentifierFirstMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithIdentifierFirstMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithCodeMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithLookupSecretMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithLookupSecretMethod) - if err == nil { - jsonUpdateLoginFlowWithLookupSecretMethod, _ := json.Marshal(dst.UpdateLoginFlowWithLookupSecretMethod) - if string(jsonUpdateLoginFlowWithLookupSecretMethod) == "{}" { // empty struct - dst.UpdateLoginFlowWithLookupSecretMethod = nil + // check if the discriminator value is 'lookup_secret' + if jsonDict["method"] == "lookup_secret" { + // try to unmarshal JSON data into UpdateLoginFlowWithLookupSecretMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithLookupSecretMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithLookupSecretMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithLookupSecretMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithLookupSecretMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithLookupSecretMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithOidcMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithOidcMethod) - if err == nil { - jsonUpdateLoginFlowWithOidcMethod, _ := json.Marshal(dst.UpdateLoginFlowWithOidcMethod) - if string(jsonUpdateLoginFlowWithOidcMethod) == "{}" { // empty struct + // check if the discriminator value is 'oidc' + if jsonDict["method"] == "oidc" { + // try to unmarshal JSON data into UpdateLoginFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithOidcMethod, return on the first match + } else { dst.UpdateLoginFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithOidcMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'passkey' + if jsonDict["method"] == "passkey" { + // try to unmarshal JSON data into UpdateLoginFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithPasskeyMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasskeyMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithOidcMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithPasswordMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithPasswordMethod) - if err == nil { - jsonUpdateLoginFlowWithPasswordMethod, _ := json.Marshal(dst.UpdateLoginFlowWithPasswordMethod) - if string(jsonUpdateLoginFlowWithPasswordMethod) == "{}" { // empty struct - dst.UpdateLoginFlowWithPasswordMethod = nil + // check if the discriminator value is 'password' + if jsonDict["method"] == "password" { + // try to unmarshal JSON data into UpdateLoginFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithPasswordMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasswordMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithPasswordMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithTotpMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithTotpMethod) - if err == nil { - jsonUpdateLoginFlowWithTotpMethod, _ := json.Marshal(dst.UpdateLoginFlowWithTotpMethod) - if string(jsonUpdateLoginFlowWithTotpMethod) == "{}" { // empty struct - dst.UpdateLoginFlowWithTotpMethod = nil + // check if the discriminator value is 'totp' + if jsonDict["method"] == "totp" { + // try to unmarshal JSON data into UpdateLoginFlowWithTotpMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithTotpMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithTotpMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithTotpMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithTotpMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithTotpMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithWebAuthnMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithWebAuthnMethod) - if err == nil { - jsonUpdateLoginFlowWithWebAuthnMethod, _ := json.Marshal(dst.UpdateLoginFlowWithWebAuthnMethod) - if string(jsonUpdateLoginFlowWithWebAuthnMethod) == "{}" { // empty struct + // check if the discriminator value is 'webauthn' + if jsonDict["method"] == "webauthn" { + // try to unmarshal JSON data into UpdateLoginFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithWebAuthnMethod, return on the first match + } else { dst.UpdateLoginFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithWebAuthnMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateLoginFlowWithCodeMethod' + if jsonDict["method"] == "updateLoginFlowWithCodeMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithWebAuthnMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateLoginFlowWithCodeMethod = nil - dst.UpdateLoginFlowWithLookupSecretMethod = nil - dst.UpdateLoginFlowWithOidcMethod = nil - dst.UpdateLoginFlowWithPasswordMethod = nil - dst.UpdateLoginFlowWithTotpMethod = nil - dst.UpdateLoginFlowWithWebAuthnMethod = nil + // check if the discriminator value is 'updateLoginFlowWithIdentifierFirstMethod' + if jsonDict["method"] == "updateLoginFlowWithIdentifierFirstMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithIdentifierFirstMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithIdentifierFirstMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithIdentifierFirstMethod, return on the first match + } else { + dst.UpdateLoginFlowWithIdentifierFirstMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithIdentifierFirstMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateLoginFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateLoginFlowBody)") + // check if the discriminator value is 'updateLoginFlowWithLookupSecretMethod' + if jsonDict["method"] == "updateLoginFlowWithLookupSecretMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithLookupSecretMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithLookupSecretMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithLookupSecretMethod, return on the first match + } else { + dst.UpdateLoginFlowWithLookupSecretMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithLookupSecretMethod: %s", err.Error()) + } } + + // check if the discriminator value is 'updateLoginFlowWithOidcMethod' + if jsonDict["method"] == "updateLoginFlowWithOidcMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithOidcMethod, return on the first match + } else { + dst.UpdateLoginFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithOidcMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateLoginFlowWithPasskeyMethod' + if jsonDict["method"] == "updateLoginFlowWithPasskeyMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithPasskeyMethod, return on the first match + } else { + dst.UpdateLoginFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasskeyMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateLoginFlowWithPasswordMethod' + if jsonDict["method"] == "updateLoginFlowWithPasswordMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithPasswordMethod, return on the first match + } else { + dst.UpdateLoginFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasswordMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateLoginFlowWithTotpMethod' + if jsonDict["method"] == "updateLoginFlowWithTotpMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithTotpMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithTotpMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithTotpMethod, return on the first match + } else { + dst.UpdateLoginFlowWithTotpMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithTotpMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateLoginFlowWithWebAuthnMethod' + if jsonDict["method"] == "updateLoginFlowWithWebAuthnMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithWebAuthnMethod, return on the first match + } else { + dst.UpdateLoginFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithWebAuthnMethod: %s", err.Error()) + } + } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON @@ -173,6 +295,10 @@ func (src UpdateLoginFlowBody) MarshalJSON() ([]byte, error) { return json.Marshal(&src.UpdateLoginFlowWithCodeMethod) } + if src.UpdateLoginFlowWithIdentifierFirstMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithIdentifierFirstMethod) + } + if src.UpdateLoginFlowWithLookupSecretMethod != nil { return json.Marshal(&src.UpdateLoginFlowWithLookupSecretMethod) } @@ -181,6 +307,10 @@ func (src UpdateLoginFlowBody) MarshalJSON() ([]byte, error) { return json.Marshal(&src.UpdateLoginFlowWithOidcMethod) } + if src.UpdateLoginFlowWithPasskeyMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithPasskeyMethod) + } + if src.UpdateLoginFlowWithPasswordMethod != nil { return json.Marshal(&src.UpdateLoginFlowWithPasswordMethod) } @@ -205,6 +335,10 @@ func (obj *UpdateLoginFlowBody) GetActualInstance() interface{} { return obj.UpdateLoginFlowWithCodeMethod } + if obj.UpdateLoginFlowWithIdentifierFirstMethod != nil { + return obj.UpdateLoginFlowWithIdentifierFirstMethod + } + if obj.UpdateLoginFlowWithLookupSecretMethod != nil { return obj.UpdateLoginFlowWithLookupSecretMethod } @@ -213,6 +347,10 @@ func (obj *UpdateLoginFlowBody) GetActualInstance() interface{} { return obj.UpdateLoginFlowWithOidcMethod } + if obj.UpdateLoginFlowWithPasskeyMethod != nil { + return obj.UpdateLoginFlowWithPasskeyMethod + } + if obj.UpdateLoginFlowWithPasswordMethod != nil { return obj.UpdateLoginFlowWithPasswordMethod } diff --git a/internal/client-go/model_update_login_flow_with_code_method.go b/internal/client-go/model_update_login_flow_with_code_method.go index bd97ab583ebc..06272618da90 100644 --- a/internal/client-go/model_update_login_flow_with_code_method.go +++ b/internal/client-go/model_update_login_flow_with_code_method.go @@ -17,6 +17,8 @@ import ( // UpdateLoginFlowWithCodeMethod Update Login flow using the code method type UpdateLoginFlowWithCodeMethod struct { + // Address is the address to send the code to, in case that there are multiple addresses. This field is only used in two-factor flows and is ineffective for passwordless flows. + Address *string `json:"address,omitempty"` // Code is the 6 digits code sent to the user Code *string `json:"code,omitempty"` // CSRFToken is the anti-CSRF token @@ -27,6 +29,8 @@ type UpdateLoginFlowWithCodeMethod struct { Method string `json:"method"` // Resend is set when the user wants to resend the code Resend *string `json:"resend,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateLoginFlowWithCodeMethod instantiates a new UpdateLoginFlowWithCodeMethod object @@ -48,6 +52,38 @@ func NewUpdateLoginFlowWithCodeMethodWithDefaults() *UpdateLoginFlowWithCodeMeth return &this } +// GetAddress returns the Address field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetAddress() string { + if o == nil || o.Address == nil { + var ret string + return ret + } + return *o.Address +} + +// GetAddressOk returns a tuple with the Address field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetAddressOk() (*string, bool) { + if o == nil || o.Address == nil { + return nil, false + } + return o.Address, true +} + +// HasAddress returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasAddress() bool { + if o != nil && o.Address != nil { + return true + } + + return false +} + +// SetAddress gets a reference to the given string and assigns it to the Address field. +func (o *UpdateLoginFlowWithCodeMethod) SetAddress(v string) { + o.Address = &v +} + // GetCode returns the Code field value if set, zero value otherwise. func (o *UpdateLoginFlowWithCodeMethod) GetCode() string { if o == nil || o.Code == nil { @@ -192,8 +228,43 @@ func (o *UpdateLoginFlowWithCodeMethod) SetResend(v string) { o.Resend = &v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithCodeMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} + if o.Address != nil { + toSerialize["address"] = o.Address + } if o.Code != nil { toSerialize["code"] = o.Code } @@ -209,6 +280,9 @@ func (o UpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { if o.Resend != nil { toSerialize["resend"] = o.Resend } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_login_flow_with_identifier_first_method.go b/internal/client-go/model_update_login_flow_with_identifier_first_method.go new file mode 100644 index 000000000000..70cf8002990d --- /dev/null +++ b/internal/client-go/model_update_login_flow_with_identifier_first_method.go @@ -0,0 +1,212 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateLoginFlowWithIdentifierFirstMethod Update Login Flow with Multi-Step Method +type UpdateLoginFlowWithIdentifierFirstMethod struct { + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // Identifier is the email or username of the user trying to log in. + Identifier string `json:"identifier"` + // Method should be set to \"password\" when logging in using the identifier and password strategy. + Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` +} + +// NewUpdateLoginFlowWithIdentifierFirstMethod instantiates a new UpdateLoginFlowWithIdentifierFirstMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateLoginFlowWithIdentifierFirstMethod(identifier string, method string) *UpdateLoginFlowWithIdentifierFirstMethod { + this := UpdateLoginFlowWithIdentifierFirstMethod{} + this.Identifier = identifier + this.Method = method + return &this +} + +// NewUpdateLoginFlowWithIdentifierFirstMethodWithDefaults instantiates a new UpdateLoginFlowWithIdentifierFirstMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateLoginFlowWithIdentifierFirstMethodWithDefaults() *UpdateLoginFlowWithIdentifierFirstMethod { + this := UpdateLoginFlowWithIdentifierFirstMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetIdentifier returns the Identifier field value +func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetIdentifier() string { + if o == nil { + var ret string + return ret + } + + return o.Identifier +} + +// GetIdentifierOk returns a tuple with the Identifier field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetIdentifierOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Identifier, true +} + +// SetIdentifier sets field value +func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetIdentifier(v string) { + o.Identifier = v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetMethod(v string) { + o.Method = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateLoginFlowWithIdentifierFirstMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["identifier"] = o.Identifier + } + if true { + toSerialize["method"] = o.Method + } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } + return json.Marshal(toSerialize) +} + +type NullableUpdateLoginFlowWithIdentifierFirstMethod struct { + value *UpdateLoginFlowWithIdentifierFirstMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithIdentifierFirstMethod) Get() *UpdateLoginFlowWithIdentifierFirstMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithIdentifierFirstMethod) Set(val *UpdateLoginFlowWithIdentifierFirstMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithIdentifierFirstMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithIdentifierFirstMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithIdentifierFirstMethod(val *UpdateLoginFlowWithIdentifierFirstMethod) *NullableUpdateLoginFlowWithIdentifierFirstMethod { + return &NullableUpdateLoginFlowWithIdentifierFirstMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithIdentifierFirstMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithIdentifierFirstMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_update_login_flow_with_oidc_method.go b/internal/client-go/model_update_login_flow_with_oidc_method.go index c196eb2121da..cdd5c665bdc5 100644 --- a/internal/client-go/model_update_login_flow_with_oidc_method.go +++ b/internal/client-go/model_update_login_flow_with_oidc_method.go @@ -19,7 +19,7 @@ import ( type UpdateLoginFlowWithOidcMethod struct { // The CSRF Token CsrfToken *string `json:"csrf_token,omitempty"` - // IDToken is an optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple + // IDToken is an optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple Google IdToken *string `json:"id_token,omitempty"` // IDTokenNonce is the nonce, used when generating the IDToken. If the provider supports nonce validation, the nonce will be validated against this value and required. IdTokenNonce *string `json:"id_token_nonce,omitempty"` @@ -29,6 +29,8 @@ type UpdateLoginFlowWithOidcMethod struct { Provider string `json:"provider"` // The identity traits. This is a placeholder for the registration flow. Traits map[string]interface{} `json:"traits,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // UpstreamParameters are the parameters that are passed to the upstream identity provider. These parameters are optional and depend on what the upstream identity provider supports. Supported parameters are: `login_hint` (string): The `login_hint` parameter suppresses the account chooser and either pre-fills the email box on the sign-in form, or selects the proper session. `hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`. `prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`. UpstreamParameters map[string]interface{} `json:"upstream_parameters,omitempty"` } @@ -228,6 +230,38 @@ func (o *UpdateLoginFlowWithOidcMethod) SetTraits(v map[string]interface{}) { o.Traits = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithOidcMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithOidcMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetUpstreamParameters returns the UpstreamParameters field value if set, zero value otherwise. func (o *UpdateLoginFlowWithOidcMethod) GetUpstreamParameters() map[string]interface{} { if o == nil || o.UpstreamParameters == nil { @@ -280,6 +314,9 @@ func (o UpdateLoginFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if o.Traits != nil { toSerialize["traits"] = o.Traits } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if o.UpstreamParameters != nil { toSerialize["upstream_parameters"] = o.UpstreamParameters } diff --git a/internal/client-go/model_update_login_flow_with_passkey_method.go b/internal/client-go/model_update_login_flow_with_passkey_method.go new file mode 100644 index 000000000000..90bbcd6ddf1c --- /dev/null +++ b/internal/client-go/model_update_login_flow_with_passkey_method.go @@ -0,0 +1,182 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateLoginFlowWithPasskeyMethod Update Login Flow with Passkey Method +type UpdateLoginFlowWithPasskeyMethod struct { + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // Method should be set to \"passkey\" when logging in using the Passkey strategy. + Method string `json:"method"` + // Login a WebAuthn Security Key This must contain the ID of the WebAuthN connection. + PasskeyLogin *string `json:"passkey_login,omitempty"` +} + +// NewUpdateLoginFlowWithPasskeyMethod instantiates a new UpdateLoginFlowWithPasskeyMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateLoginFlowWithPasskeyMethod(method string) *UpdateLoginFlowWithPasskeyMethod { + this := UpdateLoginFlowWithPasskeyMethod{} + this.Method = method + return &this +} + +// NewUpdateLoginFlowWithPasskeyMethodWithDefaults instantiates a new UpdateLoginFlowWithPasskeyMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateLoginFlowWithPasskeyMethodWithDefaults() *UpdateLoginFlowWithPasskeyMethod { + this := UpdateLoginFlowWithPasskeyMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithPasskeyMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateLoginFlowWithPasskeyMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithPasskeyMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithPasskeyMethod) SetMethod(v string) { + o.Method = v +} + +// GetPasskeyLogin returns the PasskeyLogin field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithPasskeyMethod) GetPasskeyLogin() string { + if o == nil || o.PasskeyLogin == nil { + var ret string + return ret + } + return *o.PasskeyLogin +} + +// GetPasskeyLoginOk returns a tuple with the PasskeyLogin field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) GetPasskeyLoginOk() (*string, bool) { + if o == nil || o.PasskeyLogin == nil { + return nil, false + } + return o.PasskeyLogin, true +} + +// HasPasskeyLogin returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) HasPasskeyLogin() bool { + if o != nil && o.PasskeyLogin != nil { + return true + } + + return false +} + +// SetPasskeyLogin gets a reference to the given string and assigns it to the PasskeyLogin field. +func (o *UpdateLoginFlowWithPasskeyMethod) SetPasskeyLogin(v string) { + o.PasskeyLogin = &v +} + +func (o UpdateLoginFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.PasskeyLogin != nil { + toSerialize["passkey_login"] = o.PasskeyLogin + } + return json.Marshal(toSerialize) +} + +type NullableUpdateLoginFlowWithPasskeyMethod struct { + value *UpdateLoginFlowWithPasskeyMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithPasskeyMethod) Get() *UpdateLoginFlowWithPasskeyMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithPasskeyMethod) Set(val *UpdateLoginFlowWithPasskeyMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithPasskeyMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithPasskeyMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithPasskeyMethod(val *UpdateLoginFlowWithPasskeyMethod) *NullableUpdateLoginFlowWithPasskeyMethod { + return &NullableUpdateLoginFlowWithPasskeyMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithPasskeyMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_update_login_flow_with_password_method.go b/internal/client-go/model_update_login_flow_with_password_method.go index 35a9b590bd04..4bad1a416326 100644 --- a/internal/client-go/model_update_login_flow_with_password_method.go +++ b/internal/client-go/model_update_login_flow_with_password_method.go @@ -27,6 +27,8 @@ type UpdateLoginFlowWithPasswordMethod struct { Password string `json:"password"` // Identifier is the email or username of the user trying to log in. This field is deprecated! PasswordIdentifier *string `json:"password_identifier,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateLoginFlowWithPasswordMethod instantiates a new UpdateLoginFlowWithPasswordMethod object @@ -185,6 +187,38 @@ func (o *UpdateLoginFlowWithPasswordMethod) SetPasswordIdentifier(v string) { o.PasswordIdentifier = &v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithPasswordMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasswordMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithPasswordMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithPasswordMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateLoginFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -202,6 +236,9 @@ func (o UpdateLoginFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { if o.PasswordIdentifier != nil { toSerialize["password_identifier"] = o.PasswordIdentifier } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_login_flow_with_totp_method.go b/internal/client-go/model_update_login_flow_with_totp_method.go index 34d3b316dd01..32a94efb20f4 100644 --- a/internal/client-go/model_update_login_flow_with_totp_method.go +++ b/internal/client-go/model_update_login_flow_with_totp_method.go @@ -23,6 +23,8 @@ type UpdateLoginFlowWithTotpMethod struct { Method string `json:"method"` // The TOTP code. TotpCode string `json:"totp_code"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateLoginFlowWithTotpMethod instantiates a new UpdateLoginFlowWithTotpMethod object @@ -124,6 +126,38 @@ func (o *UpdateLoginFlowWithTotpMethod) SetTotpCode(v string) { o.TotpCode = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithTotpMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithTotpMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithTotpMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithTotpMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateLoginFlowWithTotpMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateLoginFlowWithTotpMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["totp_code"] = o.TotpCode } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_login_flow_with_web_authn_method.go b/internal/client-go/model_update_login_flow_with_web_authn_method.go index d5d8554febb4..1c3211a510ed 100644 --- a/internal/client-go/model_update_login_flow_with_web_authn_method.go +++ b/internal/client-go/model_update_login_flow_with_web_authn_method.go @@ -23,6 +23,8 @@ type UpdateLoginFlowWithWebAuthnMethod struct { Identifier string `json:"identifier"` // Method should be set to \"webAuthn\" when logging in using the WebAuthn strategy. Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // Login a WebAuthn Security Key This must contain the ID of the WebAuthN connection. WebauthnLogin *string `json:"webauthn_login,omitempty"` } @@ -126,6 +128,38 @@ func (o *UpdateLoginFlowWithWebAuthnMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithWebAuthnMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithWebAuthnMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithWebAuthnMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithWebAuthnMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetWebauthnLogin returns the WebauthnLogin field value if set, zero value otherwise. func (o *UpdateLoginFlowWithWebAuthnMethod) GetWebauthnLogin() string { if o == nil || o.WebauthnLogin == nil { @@ -169,6 +203,9 @@ func (o UpdateLoginFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if o.WebauthnLogin != nil { toSerialize["webauthn_login"] = o.WebauthnLogin } diff --git a/internal/client-go/model_update_recovery_flow_body.go b/internal/client-go/model_update_recovery_flow_body.go index 6eea1d5d6b6a..b0f6de861b4f 100644 --- a/internal/client-go/model_update_recovery_flow_body.go +++ b/internal/client-go/model_update_recovery_flow_body.go @@ -39,44 +39,62 @@ func UpdateRecoveryFlowWithLinkMethodAsUpdateRecoveryFlowBody(v *UpdateRecoveryF // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateRecoveryFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateRecoveryFlowWithCodeMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRecoveryFlowWithCodeMethod) - if err == nil { - jsonUpdateRecoveryFlowWithCodeMethod, _ := json.Marshal(dst.UpdateRecoveryFlowWithCodeMethod) - if string(jsonUpdateRecoveryFlowWithCodeMethod) == "{}" { // empty struct - dst.UpdateRecoveryFlowWithCodeMethod = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'code' + if jsonDict["method"] == "code" { + // try to unmarshal JSON data into UpdateRecoveryFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateRecoveryFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateRecoveryFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateRecoveryFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateRecoveryFlowWithCodeMethod = nil } - // try to unmarshal data into UpdateRecoveryFlowWithLinkMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRecoveryFlowWithLinkMethod) - if err == nil { - jsonUpdateRecoveryFlowWithLinkMethod, _ := json.Marshal(dst.UpdateRecoveryFlowWithLinkMethod) - if string(jsonUpdateRecoveryFlowWithLinkMethod) == "{}" { // empty struct - dst.UpdateRecoveryFlowWithLinkMethod = nil + // check if the discriminator value is 'link' + if jsonDict["method"] == "link" { + // try to unmarshal JSON data into UpdateRecoveryFlowWithLinkMethod + err = json.Unmarshal(data, &dst.UpdateRecoveryFlowWithLinkMethod) + if err == nil { + return nil // data stored in dst.UpdateRecoveryFlowWithLinkMethod, return on the first match } else { - match++ + dst.UpdateRecoveryFlowWithLinkMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithLinkMethod: %s", err.Error()) } - } else { - dst.UpdateRecoveryFlowWithLinkMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateRecoveryFlowWithCodeMethod = nil - dst.UpdateRecoveryFlowWithLinkMethod = nil + // check if the discriminator value is 'updateRecoveryFlowWithCodeMethod' + if jsonDict["method"] == "updateRecoveryFlowWithCodeMethod" { + // try to unmarshal JSON data into UpdateRecoveryFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateRecoveryFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateRecoveryFlowWithCodeMethod, return on the first match + } else { + dst.UpdateRecoveryFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithCodeMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateRecoveryFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateRecoveryFlowBody)") + // check if the discriminator value is 'updateRecoveryFlowWithLinkMethod' + if jsonDict["method"] == "updateRecoveryFlowWithLinkMethod" { + // try to unmarshal JSON data into UpdateRecoveryFlowWithLinkMethod + err = json.Unmarshal(data, &dst.UpdateRecoveryFlowWithLinkMethod) + if err == nil { + return nil // data stored in dst.UpdateRecoveryFlowWithLinkMethod, return on the first match + } else { + dst.UpdateRecoveryFlowWithLinkMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithLinkMethod: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/client-go/model_update_recovery_flow_with_code_method.go b/internal/client-go/model_update_recovery_flow_with_code_method.go index 8d440330c7b8..50aad2ca2945 100644 --- a/internal/client-go/model_update_recovery_flow_with_code_method.go +++ b/internal/client-go/model_update_recovery_flow_with_code_method.go @@ -25,6 +25,8 @@ type UpdateRecoveryFlowWithCodeMethod struct { Email *string `json:"email,omitempty"` // Method is the method that should be used for this recovery flow Allowed values are `link` and `code`. link RecoveryStrategyLink code RecoveryStrategyCode Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateRecoveryFlowWithCodeMethod instantiates a new UpdateRecoveryFlowWithCodeMethod object @@ -165,6 +167,38 @@ func (o *UpdateRecoveryFlowWithCodeMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRecoveryFlowWithCodeMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateRecoveryFlowWithCodeMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.Code != nil { @@ -179,6 +213,9 @@ func (o UpdateRecoveryFlowWithCodeMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_recovery_flow_with_link_method.go b/internal/client-go/model_update_recovery_flow_with_link_method.go index 8a8861d86f07..429410cf3c01 100644 --- a/internal/client-go/model_update_recovery_flow_with_link_method.go +++ b/internal/client-go/model_update_recovery_flow_with_link_method.go @@ -23,6 +23,8 @@ type UpdateRecoveryFlowWithLinkMethod struct { Email string `json:"email"` // Method is the method that should be used for this recovery flow Allowed values are `link` and `code` link RecoveryStrategyLink code RecoveryStrategyCode Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateRecoveryFlowWithLinkMethod instantiates a new UpdateRecoveryFlowWithLinkMethod object @@ -124,6 +126,38 @@ func (o *UpdateRecoveryFlowWithLinkMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithLinkMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithLinkMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithLinkMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRecoveryFlowWithLinkMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateRecoveryFlowWithLinkMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateRecoveryFlowWithLinkMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_registration_flow_body.go b/internal/client-go/model_update_registration_flow_body.go index 0e36a95f635f..82a578cfc4d3 100644 --- a/internal/client-go/model_update_registration_flow_body.go +++ b/internal/client-go/model_update_registration_flow_body.go @@ -20,7 +20,9 @@ import ( type UpdateRegistrationFlowBody struct { UpdateRegistrationFlowWithCodeMethod *UpdateRegistrationFlowWithCodeMethod UpdateRegistrationFlowWithOidcMethod *UpdateRegistrationFlowWithOidcMethod + UpdateRegistrationFlowWithPasskeyMethod *UpdateRegistrationFlowWithPasskeyMethod UpdateRegistrationFlowWithPasswordMethod *UpdateRegistrationFlowWithPasswordMethod + UpdateRegistrationFlowWithProfileMethod *UpdateRegistrationFlowWithProfileMethod UpdateRegistrationFlowWithWebAuthnMethod *UpdateRegistrationFlowWithWebAuthnMethod } @@ -38,6 +40,13 @@ func UpdateRegistrationFlowWithOidcMethodAsUpdateRegistrationFlowBody(v *UpdateR } } +// UpdateRegistrationFlowWithPasskeyMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithPasskeyMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithPasskeyMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithPasskeyMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithPasskeyMethod: v, + } +} + // UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithPasswordMethod wrapped in UpdateRegistrationFlowBody func UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithPasswordMethod) UpdateRegistrationFlowBody { return UpdateRegistrationFlowBody{ @@ -45,6 +54,13 @@ func UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody(v *Upd } } +// UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithProfileMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithProfileMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithProfileMethod: v, + } +} + // UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithWebAuthnMethod wrapped in UpdateRegistrationFlowBody func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithWebAuthnMethod) UpdateRegistrationFlowBody { return UpdateRegistrationFlowBody{ @@ -55,72 +71,158 @@ func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *Upd // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateRegistrationFlowWithCodeMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithCodeMethod) - if err == nil { - jsonUpdateRegistrationFlowWithCodeMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithCodeMethod) - if string(jsonUpdateRegistrationFlowWithCodeMethod) == "{}" { // empty struct - dst.UpdateRegistrationFlowWithCodeMethod = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'code' + if jsonDict["method"] == "code" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateRegistrationFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateRegistrationFlowWithCodeMethod = nil } - // try to unmarshal data into UpdateRegistrationFlowWithOidcMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithOidcMethod) - if err == nil { - jsonUpdateRegistrationFlowWithOidcMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithOidcMethod) - if string(jsonUpdateRegistrationFlowWithOidcMethod) == "{}" { // empty struct + // check if the discriminator value is 'oidc' + if jsonDict["method"] == "oidc" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithOidcMethod, return on the first match + } else { dst.UpdateRegistrationFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithOidcMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'passkey' + if jsonDict["method"] == "passkey" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithPasskeyMethod, return on the first match } else { - match++ + dst.UpdateRegistrationFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithPasskeyMethod: %s", err.Error()) } - } else { - dst.UpdateRegistrationFlowWithOidcMethod = nil } - // try to unmarshal data into UpdateRegistrationFlowWithPasswordMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithPasswordMethod) - if err == nil { - jsonUpdateRegistrationFlowWithPasswordMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithPasswordMethod) - if string(jsonUpdateRegistrationFlowWithPasswordMethod) == "{}" { // empty struct + // check if the discriminator value is 'password' + if jsonDict["method"] == "password" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithPasswordMethod, return on the first match + } else { dst.UpdateRegistrationFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithPasswordMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'profile' + if jsonDict["method"] == "profile" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match } else { - match++ + dst.UpdateRegistrationFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error()) } - } else { - dst.UpdateRegistrationFlowWithPasswordMethod = nil } - // try to unmarshal data into UpdateRegistrationFlowWithWebAuthnMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithWebAuthnMethod) - if err == nil { - jsonUpdateRegistrationFlowWithWebAuthnMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithWebAuthnMethod) - if string(jsonUpdateRegistrationFlowWithWebAuthnMethod) == "{}" { // empty struct + // check if the discriminator value is 'webauthn' + if jsonDict["method"] == "webauthn" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithWebAuthnMethod, return on the first match + } else { dst.UpdateRegistrationFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithWebAuthnMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateRegistrationFlowWithCodeMethod' + if jsonDict["method"] == "updateRegistrationFlowWithCodeMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateRegistrationFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateRegistrationFlowWithWebAuthnMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateRegistrationFlowWithCodeMethod = nil - dst.UpdateRegistrationFlowWithOidcMethod = nil - dst.UpdateRegistrationFlowWithPasswordMethod = nil - dst.UpdateRegistrationFlowWithWebAuthnMethod = nil + // check if the discriminator value is 'updateRegistrationFlowWithOidcMethod' + if jsonDict["method"] == "updateRegistrationFlowWithOidcMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithOidcMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithOidcMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateRegistrationFlowWithPasskeyMethod' + if jsonDict["method"] == "updateRegistrationFlowWithPasskeyMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithPasskeyMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithPasskeyMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateRegistrationFlowWithPasswordMethod' + if jsonDict["method"] == "updateRegistrationFlowWithPasswordMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithPasswordMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithPasswordMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateRegistrationFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateRegistrationFlowBody)") + // check if the discriminator value is 'updateRegistrationFlowWithProfileMethod' + if jsonDict["method"] == "updateRegistrationFlowWithProfileMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error()) + } } + + // check if the discriminator value is 'updateRegistrationFlowWithWebAuthnMethod' + if jsonDict["method"] == "updateRegistrationFlowWithWebAuthnMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithWebAuthnMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithWebAuthnMethod: %s", err.Error()) + } + } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON @@ -133,10 +235,18 @@ func (src UpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) { return json.Marshal(&src.UpdateRegistrationFlowWithOidcMethod) } + if src.UpdateRegistrationFlowWithPasskeyMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithPasskeyMethod) + } + if src.UpdateRegistrationFlowWithPasswordMethod != nil { return json.Marshal(&src.UpdateRegistrationFlowWithPasswordMethod) } + if src.UpdateRegistrationFlowWithProfileMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithProfileMethod) + } + if src.UpdateRegistrationFlowWithWebAuthnMethod != nil { return json.Marshal(&src.UpdateRegistrationFlowWithWebAuthnMethod) } @@ -157,10 +267,18 @@ func (obj *UpdateRegistrationFlowBody) GetActualInstance() interface{} { return obj.UpdateRegistrationFlowWithOidcMethod } + if obj.UpdateRegistrationFlowWithPasskeyMethod != nil { + return obj.UpdateRegistrationFlowWithPasskeyMethod + } + if obj.UpdateRegistrationFlowWithPasswordMethod != nil { return obj.UpdateRegistrationFlowWithPasswordMethod } + if obj.UpdateRegistrationFlowWithProfileMethod != nil { + return obj.UpdateRegistrationFlowWithProfileMethod + } + if obj.UpdateRegistrationFlowWithWebAuthnMethod != nil { return obj.UpdateRegistrationFlowWithWebAuthnMethod } diff --git a/internal/client-go/model_update_registration_flow_with_oidc_method.go b/internal/client-go/model_update_registration_flow_with_oidc_method.go index 509e978a0627..2ee32605fee6 100644 --- a/internal/client-go/model_update_registration_flow_with_oidc_method.go +++ b/internal/client-go/model_update_registration_flow_with_oidc_method.go @@ -19,7 +19,7 @@ import ( type UpdateRegistrationFlowWithOidcMethod struct { // The CSRF Token CsrfToken *string `json:"csrf_token,omitempty"` - // IDToken is an optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple + // IDToken is an optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple Google IdToken *string `json:"id_token,omitempty"` // IDTokenNonce is the nonce, used when generating the IDToken. If the provider supports nonce validation, the nonce will be validated against this value and is required. IdTokenNonce *string `json:"id_token_nonce,omitempty"` diff --git a/internal/client-go/model_update_registration_flow_with_passkey_method.go b/internal/client-go/model_update_registration_flow_with_passkey_method.go new file mode 100644 index 000000000000..38d59713262e --- /dev/null +++ b/internal/client-go/model_update_registration_flow_with_passkey_method.go @@ -0,0 +1,249 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateRegistrationFlowWithPasskeyMethod Update Registration Flow with Passkey Method +type UpdateRegistrationFlowWithPasskeyMethod struct { + // CSRFToken is the anti-CSRF token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method Should be set to \"passkey\" when trying to add, update, or remove a Passkey. + Method string `json:"method"` + // Register a WebAuthn Security Key It is expected that the JSON returned by the WebAuthn registration process is included here. + PasskeyRegister *string `json:"passkey_register,omitempty"` + // The identity's traits + Traits map[string]interface{} `json:"traits"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` +} + +// NewUpdateRegistrationFlowWithPasskeyMethod instantiates a new UpdateRegistrationFlowWithPasskeyMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateRegistrationFlowWithPasskeyMethod(method string, traits map[string]interface{}) *UpdateRegistrationFlowWithPasskeyMethod { + this := UpdateRegistrationFlowWithPasskeyMethod{} + this.Method = method + this.Traits = traits + return &this +} + +// NewUpdateRegistrationFlowWithPasskeyMethodWithDefaults instantiates a new UpdateRegistrationFlowWithPasskeyMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateRegistrationFlowWithPasskeyMethodWithDefaults() *UpdateRegistrationFlowWithPasskeyMethod { + this := UpdateRegistrationFlowWithPasskeyMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetMethod(v string) { + o.Method = v +} + +// GetPasskeyRegister returns the PasskeyRegister field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetPasskeyRegister() string { + if o == nil || o.PasskeyRegister == nil { + var ret string + return ret + } + return *o.PasskeyRegister +} + +// GetPasskeyRegisterOk returns a tuple with the PasskeyRegister field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetPasskeyRegisterOk() (*string, bool) { + if o == nil || o.PasskeyRegister == nil { + return nil, false + } + return o.PasskeyRegister, true +} + +// HasPasskeyRegister returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) HasPasskeyRegister() bool { + if o != nil && o.PasskeyRegister != nil { + return true + } + + return false +} + +// SetPasskeyRegister gets a reference to the given string and assigns it to the PasskeyRegister field. +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetPasskeyRegister(v string) { + o.PasskeyRegister = &v +} + +// GetTraits returns the Traits field value +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetTraits() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return nil, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateRegistrationFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.PasskeyRegister != nil { + toSerialize["passkey_register"] = o.PasskeyRegister + } + if true { + toSerialize["traits"] = o.Traits + } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } + return json.Marshal(toSerialize) +} + +type NullableUpdateRegistrationFlowWithPasskeyMethod struct { + value *UpdateRegistrationFlowWithPasskeyMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithPasskeyMethod) Get() *UpdateRegistrationFlowWithPasskeyMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithPasskeyMethod) Set(val *UpdateRegistrationFlowWithPasskeyMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithPasskeyMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithPasskeyMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithPasskeyMethod(val *UpdateRegistrationFlowWithPasskeyMethod) *NullableUpdateRegistrationFlowWithPasskeyMethod { + return &NullableUpdateRegistrationFlowWithPasskeyMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithPasskeyMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_update_registration_flow_with_profile_method.go b/internal/client-go/model_update_registration_flow_with_profile_method.go new file mode 100644 index 000000000000..8cdbb2eab764 --- /dev/null +++ b/internal/client-go/model_update_registration_flow_with_profile_method.go @@ -0,0 +1,249 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateRegistrationFlowWithProfileMethod Update Registration Flow with Profile Method +type UpdateRegistrationFlowWithProfileMethod struct { + // The Anti-CSRF Token This token is only required when performing browser flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // Method Should be set to profile when trying to update a profile. + Method string `json:"method"` + // Screen requests navigation to a previous screen. This must be set to credential-selection to go back to the credential selection screen. credential-selection RegistrationScreenCredentialSelection nolint:gosec // not a credential previous RegistrationScreenPrevious + Screen *string `json:"screen,omitempty"` + // Traits The identity's traits. + Traits map[string]interface{} `json:"traits"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` +} + +// NewUpdateRegistrationFlowWithProfileMethod instantiates a new UpdateRegistrationFlowWithProfileMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateRegistrationFlowWithProfileMethod(method string, traits map[string]interface{}) *UpdateRegistrationFlowWithProfileMethod { + this := UpdateRegistrationFlowWithProfileMethod{} + this.Method = method + this.Traits = traits + return &this +} + +// NewUpdateRegistrationFlowWithProfileMethodWithDefaults instantiates a new UpdateRegistrationFlowWithProfileMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateRegistrationFlowWithProfileMethodWithDefaults() *UpdateRegistrationFlowWithProfileMethod { + this := UpdateRegistrationFlowWithProfileMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithProfileMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithProfileMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithProfileMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithProfileMethod) SetMethod(v string) { + o.Method = v +} + +// GetScreen returns the Screen field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithProfileMethod) GetScreen() string { + if o == nil || o.Screen == nil { + var ret string + return ret + } + return *o.Screen +} + +// GetScreenOk returns a tuple with the Screen field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetScreenOk() (*string, bool) { + if o == nil || o.Screen == nil { + return nil, false + } + return o.Screen, true +} + +// HasScreen returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) HasScreen() bool { + if o != nil && o.Screen != nil { + return true + } + + return false +} + +// SetScreen gets a reference to the given string and assigns it to the Screen field. +func (o *UpdateRegistrationFlowWithProfileMethod) SetScreen(v string) { + o.Screen = &v +} + +// GetTraits returns the Traits field value +func (o *UpdateRegistrationFlowWithProfileMethod) GetTraits() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return nil, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateRegistrationFlowWithProfileMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithProfileMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithProfileMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateRegistrationFlowWithProfileMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.Screen != nil { + toSerialize["screen"] = o.Screen + } + if true { + toSerialize["traits"] = o.Traits + } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } + return json.Marshal(toSerialize) +} + +type NullableUpdateRegistrationFlowWithProfileMethod struct { + value *UpdateRegistrationFlowWithProfileMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithProfileMethod) Get() *UpdateRegistrationFlowWithProfileMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithProfileMethod) Set(val *UpdateRegistrationFlowWithProfileMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithProfileMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithProfileMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithProfileMethod(val *UpdateRegistrationFlowWithProfileMethod) *NullableUpdateRegistrationFlowWithProfileMethod { + return &NullableUpdateRegistrationFlowWithProfileMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithProfileMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithProfileMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_update_settings_flow_body.go b/internal/client-go/model_update_settings_flow_body.go index 064ff9b771dd..cb1edae41d31 100644 --- a/internal/client-go/model_update_settings_flow_body.go +++ b/internal/client-go/model_update_settings_flow_body.go @@ -20,6 +20,7 @@ import ( type UpdateSettingsFlowBody struct { UpdateSettingsFlowWithLookupMethod *UpdateSettingsFlowWithLookupMethod UpdateSettingsFlowWithOidcMethod *UpdateSettingsFlowWithOidcMethod + UpdateSettingsFlowWithPasskeyMethod *UpdateSettingsFlowWithPasskeyMethod UpdateSettingsFlowWithPasswordMethod *UpdateSettingsFlowWithPasswordMethod UpdateSettingsFlowWithProfileMethod *UpdateSettingsFlowWithProfileMethod UpdateSettingsFlowWithTotpMethod *UpdateSettingsFlowWithTotpMethod @@ -40,6 +41,13 @@ func UpdateSettingsFlowWithOidcMethodAsUpdateSettingsFlowBody(v *UpdateSettingsF } } +// UpdateSettingsFlowWithPasskeyMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithPasskeyMethod wrapped in UpdateSettingsFlowBody +func UpdateSettingsFlowWithPasskeyMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithPasskeyMethod) UpdateSettingsFlowBody { + return UpdateSettingsFlowBody{ + UpdateSettingsFlowWithPasskeyMethod: v, + } +} + // UpdateSettingsFlowWithPasswordMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithPasswordMethod wrapped in UpdateSettingsFlowBody func UpdateSettingsFlowWithPasswordMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithPasswordMethod) UpdateSettingsFlowBody { return UpdateSettingsFlowBody{ @@ -71,100 +79,182 @@ func UpdateSettingsFlowWithWebAuthnMethodAsUpdateSettingsFlowBody(v *UpdateSetti // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateSettingsFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateSettingsFlowWithLookupMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithLookupMethod) - if err == nil { - jsonUpdateSettingsFlowWithLookupMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithLookupMethod) - if string(jsonUpdateSettingsFlowWithLookupMethod) == "{}" { // empty struct - dst.UpdateSettingsFlowWithLookupMethod = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'lookup_secret' + if jsonDict["method"] == "lookup_secret" { + // try to unmarshal JSON data into UpdateSettingsFlowWithLookupMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithLookupMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithLookupMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithLookupMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithLookupMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithLookupMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithOidcMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithOidcMethod) - if err == nil { - jsonUpdateSettingsFlowWithOidcMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithOidcMethod) - if string(jsonUpdateSettingsFlowWithOidcMethod) == "{}" { // empty struct + // check if the discriminator value is 'oidc' + if jsonDict["method"] == "oidc" { + // try to unmarshal JSON data into UpdateSettingsFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithOidcMethod, return on the first match + } else { dst.UpdateSettingsFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithOidcMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'passkey' + if jsonDict["method"] == "passkey" { + // try to unmarshal JSON data into UpdateSettingsFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithPasskeyMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasskeyMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithOidcMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithPasswordMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithPasswordMethod) - if err == nil { - jsonUpdateSettingsFlowWithPasswordMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithPasswordMethod) - if string(jsonUpdateSettingsFlowWithPasswordMethod) == "{}" { // empty struct - dst.UpdateSettingsFlowWithPasswordMethod = nil + // check if the discriminator value is 'password' + if jsonDict["method"] == "password" { + // try to unmarshal JSON data into UpdateSettingsFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithPasswordMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasswordMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithPasswordMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithProfileMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithProfileMethod) - if err == nil { - jsonUpdateSettingsFlowWithProfileMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithProfileMethod) - if string(jsonUpdateSettingsFlowWithProfileMethod) == "{}" { // empty struct - dst.UpdateSettingsFlowWithProfileMethod = nil + // check if the discriminator value is 'profile' + if jsonDict["method"] == "profile" { + // try to unmarshal JSON data into UpdateSettingsFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithProfileMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithProfileMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithProfileMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithTotpMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithTotpMethod) - if err == nil { - jsonUpdateSettingsFlowWithTotpMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithTotpMethod) - if string(jsonUpdateSettingsFlowWithTotpMethod) == "{}" { // empty struct - dst.UpdateSettingsFlowWithTotpMethod = nil + // check if the discriminator value is 'totp' + if jsonDict["method"] == "totp" { + // try to unmarshal JSON data into UpdateSettingsFlowWithTotpMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithTotpMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithTotpMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithTotpMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithTotpMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithTotpMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithWebAuthnMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithWebAuthnMethod) - if err == nil { - jsonUpdateSettingsFlowWithWebAuthnMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithWebAuthnMethod) - if string(jsonUpdateSettingsFlowWithWebAuthnMethod) == "{}" { // empty struct + // check if the discriminator value is 'webauthn' + if jsonDict["method"] == "webauthn" { + // try to unmarshal JSON data into UpdateSettingsFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithWebAuthnMethod, return on the first match + } else { dst.UpdateSettingsFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithWebAuthnMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateSettingsFlowWithLookupMethod' + if jsonDict["method"] == "updateSettingsFlowWithLookupMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithLookupMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithLookupMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithLookupMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithLookupMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithLookupMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithWebAuthnMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateSettingsFlowWithLookupMethod = nil - dst.UpdateSettingsFlowWithOidcMethod = nil - dst.UpdateSettingsFlowWithPasswordMethod = nil - dst.UpdateSettingsFlowWithProfileMethod = nil - dst.UpdateSettingsFlowWithTotpMethod = nil - dst.UpdateSettingsFlowWithWebAuthnMethod = nil + // check if the discriminator value is 'updateSettingsFlowWithOidcMethod' + if jsonDict["method"] == "updateSettingsFlowWithOidcMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithOidcMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithOidcMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateSettingsFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateSettingsFlowBody)") + // check if the discriminator value is 'updateSettingsFlowWithPasskeyMethod' + if jsonDict["method"] == "updateSettingsFlowWithPasskeyMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithPasskeyMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasskeyMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateSettingsFlowWithPasswordMethod' + if jsonDict["method"] == "updateSettingsFlowWithPasswordMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithPasswordMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasswordMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateSettingsFlowWithProfileMethod' + if jsonDict["method"] == "updateSettingsFlowWithProfileMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithProfileMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithProfileMethod: %s", err.Error()) + } } + + // check if the discriminator value is 'updateSettingsFlowWithTotpMethod' + if jsonDict["method"] == "updateSettingsFlowWithTotpMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithTotpMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithTotpMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithTotpMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithTotpMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithTotpMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateSettingsFlowWithWebAuthnMethod' + if jsonDict["method"] == "updateSettingsFlowWithWebAuthnMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithWebAuthnMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithWebAuthnMethod: %s", err.Error()) + } + } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON @@ -177,6 +267,10 @@ func (src UpdateSettingsFlowBody) MarshalJSON() ([]byte, error) { return json.Marshal(&src.UpdateSettingsFlowWithOidcMethod) } + if src.UpdateSettingsFlowWithPasskeyMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithPasskeyMethod) + } + if src.UpdateSettingsFlowWithPasswordMethod != nil { return json.Marshal(&src.UpdateSettingsFlowWithPasswordMethod) } @@ -209,6 +303,10 @@ func (obj *UpdateSettingsFlowBody) GetActualInstance() interface{} { return obj.UpdateSettingsFlowWithOidcMethod } + if obj.UpdateSettingsFlowWithPasskeyMethod != nil { + return obj.UpdateSettingsFlowWithPasskeyMethod + } + if obj.UpdateSettingsFlowWithPasswordMethod != nil { return obj.UpdateSettingsFlowWithPasswordMethod } diff --git a/internal/client-go/model_update_settings_flow_with_lookup_method.go b/internal/client-go/model_update_settings_flow_with_lookup_method.go index 96a12cb7f9e2..ca2e89827126 100644 --- a/internal/client-go/model_update_settings_flow_with_lookup_method.go +++ b/internal/client-go/model_update_settings_flow_with_lookup_method.go @@ -29,6 +29,8 @@ type UpdateSettingsFlowWithLookupMethod struct { LookupSecretReveal *bool `json:"lookup_secret_reveal,omitempty"` // Method Should be set to \"lookup\" when trying to add, update, or remove a lookup pairing. Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateSettingsFlowWithLookupMethod instantiates a new UpdateSettingsFlowWithLookupMethod object @@ -233,6 +235,38 @@ func (o *UpdateSettingsFlowWithLookupMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithLookupMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithLookupMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithLookupMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithLookupMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateSettingsFlowWithLookupMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -253,6 +287,9 @@ func (o UpdateSettingsFlowWithLookupMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_settings_flow_with_oidc_method.go b/internal/client-go/model_update_settings_flow_with_oidc_method.go index 3008436d8b27..c54a0d1251f3 100644 --- a/internal/client-go/model_update_settings_flow_with_oidc_method.go +++ b/internal/client-go/model_update_settings_flow_with_oidc_method.go @@ -25,6 +25,8 @@ type UpdateSettingsFlowWithOidcMethod struct { Method string `json:"method"` // The identity's traits in: body Traits map[string]interface{} `json:"traits,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // Unlink this provider Either this or `link` must be set. type: string in: body Unlink *string `json:"unlink,omitempty"` // UpstreamParameters are the parameters that are passed to the upstream identity provider. These parameters are optional and depend on what the upstream identity provider supports. Supported parameters are: `login_hint` (string): The `login_hint` parameter suppresses the account chooser and either pre-fills the email box on the sign-in form, or selects the proper session. `hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`. `prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`. @@ -169,6 +171,38 @@ func (o *UpdateSettingsFlowWithOidcMethod) SetTraits(v map[string]interface{}) { o.Traits = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithOidcMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithOidcMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithOidcMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithOidcMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetUnlink returns the Unlink field value if set, zero value otherwise. func (o *UpdateSettingsFlowWithOidcMethod) GetUnlink() string { if o == nil || o.Unlink == nil { @@ -247,6 +281,9 @@ func (o UpdateSettingsFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if o.Traits != nil { toSerialize["traits"] = o.Traits } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if o.Unlink != nil { toSerialize["unlink"] = o.Unlink } diff --git a/internal/client-go/model_update_settings_flow_with_passkey_method.go b/internal/client-go/model_update_settings_flow_with_passkey_method.go new file mode 100644 index 000000000000..c7103432afcd --- /dev/null +++ b/internal/client-go/model_update_settings_flow_with_passkey_method.go @@ -0,0 +1,219 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateSettingsFlowWithPasskeyMethod Update Settings Flow with Passkey Method +type UpdateSettingsFlowWithPasskeyMethod struct { + // CSRFToken is the anti-CSRF token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method Should be set to \"passkey\" when trying to add, update, or remove a webAuthn pairing. + Method string `json:"method"` + // Remove a WebAuthn Security Key This must contain the ID of the WebAuthN connection. + PasskeyRemove *string `json:"passkey_remove,omitempty"` + // Register a WebAuthn Security Key It is expected that the JSON returned by the WebAuthn registration process is included here. + PasskeySettingsRegister *string `json:"passkey_settings_register,omitempty"` +} + +// NewUpdateSettingsFlowWithPasskeyMethod instantiates a new UpdateSettingsFlowWithPasskeyMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateSettingsFlowWithPasskeyMethod(method string) *UpdateSettingsFlowWithPasskeyMethod { + this := UpdateSettingsFlowWithPasskeyMethod{} + this.Method = method + return &this +} + +// NewUpdateSettingsFlowWithPasskeyMethodWithDefaults instantiates a new UpdateSettingsFlowWithPasskeyMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateSettingsFlowWithPasskeyMethodWithDefaults() *UpdateSettingsFlowWithPasskeyMethod { + this := UpdateSettingsFlowWithPasskeyMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateSettingsFlowWithPasskeyMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateSettingsFlowWithPasskeyMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateSettingsFlowWithPasskeyMethod) SetMethod(v string) { + o.Method = v +} + +// GetPasskeyRemove returns the PasskeyRemove field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetPasskeyRemove() string { + if o == nil || o.PasskeyRemove == nil { + var ret string + return ret + } + return *o.PasskeyRemove +} + +// GetPasskeyRemoveOk returns a tuple with the PasskeyRemove field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetPasskeyRemoveOk() (*string, bool) { + if o == nil || o.PasskeyRemove == nil { + return nil, false + } + return o.PasskeyRemove, true +} + +// HasPasskeyRemove returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) HasPasskeyRemove() bool { + if o != nil && o.PasskeyRemove != nil { + return true + } + + return false +} + +// SetPasskeyRemove gets a reference to the given string and assigns it to the PasskeyRemove field. +func (o *UpdateSettingsFlowWithPasskeyMethod) SetPasskeyRemove(v string) { + o.PasskeyRemove = &v +} + +// GetPasskeySettingsRegister returns the PasskeySettingsRegister field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetPasskeySettingsRegister() string { + if o == nil || o.PasskeySettingsRegister == nil { + var ret string + return ret + } + return *o.PasskeySettingsRegister +} + +// GetPasskeySettingsRegisterOk returns a tuple with the PasskeySettingsRegister field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetPasskeySettingsRegisterOk() (*string, bool) { + if o == nil || o.PasskeySettingsRegister == nil { + return nil, false + } + return o.PasskeySettingsRegister, true +} + +// HasPasskeySettingsRegister returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) HasPasskeySettingsRegister() bool { + if o != nil && o.PasskeySettingsRegister != nil { + return true + } + + return false +} + +// SetPasskeySettingsRegister gets a reference to the given string and assigns it to the PasskeySettingsRegister field. +func (o *UpdateSettingsFlowWithPasskeyMethod) SetPasskeySettingsRegister(v string) { + o.PasskeySettingsRegister = &v +} + +func (o UpdateSettingsFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.PasskeyRemove != nil { + toSerialize["passkey_remove"] = o.PasskeyRemove + } + if o.PasskeySettingsRegister != nil { + toSerialize["passkey_settings_register"] = o.PasskeySettingsRegister + } + return json.Marshal(toSerialize) +} + +type NullableUpdateSettingsFlowWithPasskeyMethod struct { + value *UpdateSettingsFlowWithPasskeyMethod + isSet bool +} + +func (v NullableUpdateSettingsFlowWithPasskeyMethod) Get() *UpdateSettingsFlowWithPasskeyMethod { + return v.value +} + +func (v *NullableUpdateSettingsFlowWithPasskeyMethod) Set(val *UpdateSettingsFlowWithPasskeyMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowWithPasskeyMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowWithPasskeyMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowWithPasskeyMethod(val *UpdateSettingsFlowWithPasskeyMethod) *NullableUpdateSettingsFlowWithPasskeyMethod { + return &NullableUpdateSettingsFlowWithPasskeyMethod{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowWithPasskeyMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_update_settings_flow_with_password_method.go b/internal/client-go/model_update_settings_flow_with_password_method.go index d4b9934756f0..450cfdc4fb2b 100644 --- a/internal/client-go/model_update_settings_flow_with_password_method.go +++ b/internal/client-go/model_update_settings_flow_with_password_method.go @@ -23,6 +23,8 @@ type UpdateSettingsFlowWithPasswordMethod struct { Method string `json:"method"` // Password is the updated password Password string `json:"password"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateSettingsFlowWithPasswordMethod instantiates a new UpdateSettingsFlowWithPasswordMethod object @@ -124,6 +126,38 @@ func (o *UpdateSettingsFlowWithPasswordMethod) SetPassword(v string) { o.Password = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithPasswordMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasswordMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasswordMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithPasswordMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateSettingsFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateSettingsFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["password"] = o.Password } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_settings_flow_with_profile_method.go b/internal/client-go/model_update_settings_flow_with_profile_method.go index af2051f6c38c..f208e2b5fb06 100644 --- a/internal/client-go/model_update_settings_flow_with_profile_method.go +++ b/internal/client-go/model_update_settings_flow_with_profile_method.go @@ -23,6 +23,8 @@ type UpdateSettingsFlowWithProfileMethod struct { Method string `json:"method"` // Traits The identity's traits. Traits map[string]interface{} `json:"traits"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateSettingsFlowWithProfileMethod instantiates a new UpdateSettingsFlowWithProfileMethod object @@ -124,6 +126,38 @@ func (o *UpdateSettingsFlowWithProfileMethod) SetTraits(v map[string]interface{} o.Traits = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithProfileMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithProfileMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithProfileMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithProfileMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateSettingsFlowWithProfileMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateSettingsFlowWithProfileMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["traits"] = o.Traits } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_settings_flow_with_totp_method.go b/internal/client-go/model_update_settings_flow_with_totp_method.go index 565f5d0e8a83..d36d5a00ab53 100644 --- a/internal/client-go/model_update_settings_flow_with_totp_method.go +++ b/internal/client-go/model_update_settings_flow_with_totp_method.go @@ -25,6 +25,8 @@ type UpdateSettingsFlowWithTotpMethod struct { TotpCode *string `json:"totp_code,omitempty"` // UnlinkTOTP if true will remove the TOTP pairing, effectively removing the credential. This can be used to set up a new TOTP device. TotpUnlink *bool `json:"totp_unlink,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateSettingsFlowWithTotpMethod instantiates a new UpdateSettingsFlowWithTotpMethod object @@ -165,6 +167,38 @@ func (o *UpdateSettingsFlowWithTotpMethod) SetTotpUnlink(v bool) { o.TotpUnlink = &v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithTotpMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithTotpMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithTotpMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithTotpMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateSettingsFlowWithTotpMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -179,6 +213,9 @@ func (o UpdateSettingsFlowWithTotpMethod) MarshalJSON() ([]byte, error) { if o.TotpUnlink != nil { toSerialize["totp_unlink"] = o.TotpUnlink } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_settings_flow_with_web_authn_method.go b/internal/client-go/model_update_settings_flow_with_web_authn_method.go index 7bcd90c82e58..d09d0def049c 100644 --- a/internal/client-go/model_update_settings_flow_with_web_authn_method.go +++ b/internal/client-go/model_update_settings_flow_with_web_authn_method.go @@ -21,6 +21,8 @@ type UpdateSettingsFlowWithWebAuthnMethod struct { CsrfToken *string `json:"csrf_token,omitempty"` // Method Should be set to \"webauthn\" when trying to add, update, or remove a webAuthn pairing. Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // Register a WebAuthn Security Key It is expected that the JSON returned by the WebAuthn registration process is included here. WebauthnRegister *string `json:"webauthn_register,omitempty"` // Name of the WebAuthn Security Key to be Added A human-readable name for the security key which will be added. @@ -103,6 +105,38 @@ func (o *UpdateSettingsFlowWithWebAuthnMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithWebAuthnMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetWebauthnRegister returns the WebauthnRegister field value if set, zero value otherwise. func (o *UpdateSettingsFlowWithWebAuthnMethod) GetWebauthnRegister() string { if o == nil || o.WebauthnRegister == nil { @@ -207,6 +241,9 @@ func (o UpdateSettingsFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if o.WebauthnRegister != nil { toSerialize["webauthn_register"] = o.WebauthnRegister } diff --git a/internal/client-go/model_update_verification_flow_body.go b/internal/client-go/model_update_verification_flow_body.go index f4e995d222c3..9065bfdbc58e 100644 --- a/internal/client-go/model_update_verification_flow_body.go +++ b/internal/client-go/model_update_verification_flow_body.go @@ -39,44 +39,62 @@ func UpdateVerificationFlowWithLinkMethodAsUpdateVerificationFlowBody(v *UpdateV // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateVerificationFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateVerificationFlowWithCodeMethod - err = newStrictDecoder(data).Decode(&dst.UpdateVerificationFlowWithCodeMethod) - if err == nil { - jsonUpdateVerificationFlowWithCodeMethod, _ := json.Marshal(dst.UpdateVerificationFlowWithCodeMethod) - if string(jsonUpdateVerificationFlowWithCodeMethod) == "{}" { // empty struct - dst.UpdateVerificationFlowWithCodeMethod = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'code' + if jsonDict["method"] == "code" { + // try to unmarshal JSON data into UpdateVerificationFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateVerificationFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateVerificationFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateVerificationFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateVerificationFlowWithCodeMethod = nil } - // try to unmarshal data into UpdateVerificationFlowWithLinkMethod - err = newStrictDecoder(data).Decode(&dst.UpdateVerificationFlowWithLinkMethod) - if err == nil { - jsonUpdateVerificationFlowWithLinkMethod, _ := json.Marshal(dst.UpdateVerificationFlowWithLinkMethod) - if string(jsonUpdateVerificationFlowWithLinkMethod) == "{}" { // empty struct - dst.UpdateVerificationFlowWithLinkMethod = nil + // check if the discriminator value is 'link' + if jsonDict["method"] == "link" { + // try to unmarshal JSON data into UpdateVerificationFlowWithLinkMethod + err = json.Unmarshal(data, &dst.UpdateVerificationFlowWithLinkMethod) + if err == nil { + return nil // data stored in dst.UpdateVerificationFlowWithLinkMethod, return on the first match } else { - match++ + dst.UpdateVerificationFlowWithLinkMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithLinkMethod: %s", err.Error()) } - } else { - dst.UpdateVerificationFlowWithLinkMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateVerificationFlowWithCodeMethod = nil - dst.UpdateVerificationFlowWithLinkMethod = nil + // check if the discriminator value is 'updateVerificationFlowWithCodeMethod' + if jsonDict["method"] == "updateVerificationFlowWithCodeMethod" { + // try to unmarshal JSON data into UpdateVerificationFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateVerificationFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateVerificationFlowWithCodeMethod, return on the first match + } else { + dst.UpdateVerificationFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithCodeMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateVerificationFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateVerificationFlowBody)") + // check if the discriminator value is 'updateVerificationFlowWithLinkMethod' + if jsonDict["method"] == "updateVerificationFlowWithLinkMethod" { + // try to unmarshal JSON data into UpdateVerificationFlowWithLinkMethod + err = json.Unmarshal(data, &dst.UpdateVerificationFlowWithLinkMethod) + if err == nil { + return nil // data stored in dst.UpdateVerificationFlowWithLinkMethod, return on the first match + } else { + dst.UpdateVerificationFlowWithLinkMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithLinkMethod: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/client-go/model_update_verification_flow_with_code_method.go b/internal/client-go/model_update_verification_flow_with_code_method.go index 4548425684b9..e6821735a296 100644 --- a/internal/client-go/model_update_verification_flow_with_code_method.go +++ b/internal/client-go/model_update_verification_flow_with_code_method.go @@ -25,6 +25,8 @@ type UpdateVerificationFlowWithCodeMethod struct { Email *string `json:"email,omitempty"` // Method is the method that should be used for this verification flow Allowed values are `link` and `code`. link VerificationStrategyLink code VerificationStrategyCode Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateVerificationFlowWithCodeMethod instantiates a new UpdateVerificationFlowWithCodeMethod object @@ -165,6 +167,38 @@ func (o *UpdateVerificationFlowWithCodeMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateVerificationFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateVerificationFlowWithCodeMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateVerificationFlowWithCodeMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateVerificationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.Code != nil { @@ -179,6 +213,9 @@ func (o UpdateVerificationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_update_verification_flow_with_link_method.go b/internal/client-go/model_update_verification_flow_with_link_method.go index 8ebbbd749952..b7ab49d3d086 100644 --- a/internal/client-go/model_update_verification_flow_with_link_method.go +++ b/internal/client-go/model_update_verification_flow_with_link_method.go @@ -23,6 +23,8 @@ type UpdateVerificationFlowWithLinkMethod struct { Email string `json:"email"` // Method is the method that should be used for this verification flow Allowed values are `link` and `code` link VerificationStrategyLink code VerificationStrategyCode Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateVerificationFlowWithLinkMethod instantiates a new UpdateVerificationFlowWithLinkMethod object @@ -124,6 +126,38 @@ func (o *UpdateVerificationFlowWithLinkMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateVerificationFlowWithLinkMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateVerificationFlowWithLinkMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithLinkMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateVerificationFlowWithLinkMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateVerificationFlowWithLinkMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateVerificationFlowWithLinkMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_verification_flow.go b/internal/client-go/model_verification_flow.go index c10870c9f841..ae3039ddee24 100644 --- a/internal/client-go/model_verification_flow.go +++ b/internal/client-go/model_verification_flow.go @@ -32,6 +32,8 @@ type VerificationFlow struct { ReturnTo *string `json:"return_to,omitempty"` // State represents the state of this request: choose_method: ask the user to choose a method (e.g. verify your email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the verification challenge was passed. State interface{} `json:"state"` + // TransientPayload is used to pass data from the verification flow to hooks and email templates + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -268,6 +270,38 @@ func (o *VerificationFlow) SetState(v interface{}) { o.State = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *VerificationFlow) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *VerificationFlow) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *VerificationFlow) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetType returns the Type field value func (o *VerificationFlow) GetType() string { if o == nil { @@ -339,6 +373,9 @@ func (o VerificationFlow) MarshalJSON() ([]byte, error) { if o.State != nil { toSerialize["state"] = o.State } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if true { toSerialize["type"] = o.Type } diff --git a/internal/client-go/model_verification_flow_state.go b/internal/client-go/model_verification_flow_state.go index bea74568c94d..56b65e0c0a5b 100644 --- a/internal/client-go/model_verification_flow_state.go +++ b/internal/client-go/model_verification_flow_state.go @@ -16,7 +16,7 @@ import ( "fmt" ) -// VerificationFlowState The state represents the state of the verification flow. choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. +// VerificationFlowState The experimental state represents the state of a verification flow. This field is EXPERIMENTAL and subject to change! type VerificationFlowState string // List of verificationFlowState diff --git a/internal/driver.go b/internal/driver.go index 7689b7d28c24..5fa85cf08683 100644 --- a/internal/driver.go +++ b/internal/driver.go @@ -10,9 +10,13 @@ import ( "testing" "time" - "github.com/sirupsen/logrus" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" "github.com/ory/x/contextx" + "github.com/ory/x/randx" + + "github.com/sirupsen/logrus" + "github.com/ory/x/jsonnetsecure" "github.com/gofrs/uuid" @@ -37,9 +41,8 @@ func init() { }) } -func NewConfigurationWithDefaults(t testing.TB) *config.Config { - c := config.MustNew(t, logrusx.New("", ""), - os.Stderr, +func NewConfigurationWithDefaults(t testing.TB, opts ...configx.OptionModifier) *config.Config { + configOpts := append([]configx.OptionModifier{ configx.WithValues(map[string]interface{}{ "log.level": "error", config.ViperKeyDSN: dbal.NewSQLiteTestDatabase(t), @@ -55,16 +58,22 @@ func NewConfigurationWithDefaults(t testing.TB) *config.Config { config.ViperKeySecretsCipher: []string{"secret-thirty-two-character-long"}, config.ViperKeyDatabaseCleanupBatchSize: 100, config.ViperKeyDatabaseCleanupSleepTables: 1 * time.Minute, + config.ViperKeySelfServiceLoginFlowStyle: "unified", }), configx.SkipValidation(), + }, opts...) + c := config.MustNew(t, logrusx.New("", ""), + os.Stderr, + &confighelpers.TestConfigProvider{Contextualizer: &contextx.Default{}, Options: configOpts}, + configOpts..., ) return c } // NewFastRegistryWithMocks returns a registry with several mocks and an SQLite in memory database that make testing // easier and way faster. This suite does not work for e2e or advanced integration tests. -func NewFastRegistryWithMocks(t *testing.T) (*config.Config, *driver.RegistryDefault) { - conf, reg := NewRegistryDefaultWithDSN(t, "") +func NewFastRegistryWithMocks(t *testing.T, opts ...configx.OptionModifier) (*config.Config, *driver.RegistryDefault) { + conf, reg := NewRegistryDefaultWithDSN(t, "", opts...) reg.WithCSRFTokenGenerator(x.FakeCSRFTokenGenerator) reg.WithCSRFHandler(x.NewFakeCSRFHandler("")) reg.WithHooks(map[string]func(config.SelfServiceHook) interface{}{ @@ -80,16 +89,21 @@ func NewFastRegistryWithMocks(t *testing.T) (*config.Config, *driver.RegistryDef } // NewRegistryDefaultWithDSN returns a more standard registry without mocks. Good for e2e and advanced integration testing! -func NewRegistryDefaultWithDSN(t testing.TB, dsn string) (*config.Config, *driver.RegistryDefault) { +func NewRegistryDefaultWithDSN(t testing.TB, dsn string, opts ...configx.OptionModifier) (*config.Config, *driver.RegistryDefault) { ctx := context.Background() - c := NewConfigurationWithDefaults(t) - c.MustSet(ctx, config.ViperKeyDSN, stringsx.Coalesce(dsn, dbal.NewSQLiteTestDatabase(t))) + c := NewConfigurationWithDefaults(t, append([]configx.OptionModifier{configx.WithValues(map[string]interface{}{ + config.ViperKeyDSN: stringsx.Coalesce(dsn, dbal.NewSQLiteTestDatabase(t)+"&lock=false&max_conns=1"), + "dev": true, + config.ViperKeySecretsCipher: []string{randx.MustString(32, randx.AlphaNum)}, + config.ViperKeySecretsCookie: []string{randx.MustString(32, randx.AlphaNum)}, + config.ViperKeySecretsDefault: []string{randx.MustString(32, randx.AlphaNum)}, + config.ViperKeyCipherAlgorithm: "xchacha20-poly1305", + })}, opts...)...) reg, err := driver.NewRegistryFromDSN(ctx, c, logrusx.New("", "", logrusx.ForceLevel(logrus.ErrorLevel))) require.NoError(t, err) - reg.Config().MustSet(ctx, "dev", true) pool := jsonnetsecure.NewProcessPool(runtime.GOMAXPROCS(0)) t.Cleanup(pool.Close) - require.NoError(t, reg.Init(context.Background(), &contextx.Default{}, driver.SkipNetworkInit, driver.WithDisabledMigrationLogging(), driver.WithJsonnetPool(pool))) + require.NoError(t, reg.Init(context.Background(), &confighelpers.TestConfigProvider{Contextualizer: &contextx.Default{}}, driver.SkipNetworkInit, driver.WithDisabledMigrationLogging(), driver.WithJsonnetPool(pool))) require.NoError(t, reg.Persister().MigrateUp(context.Background())) // always migrate up actual, err := reg.Persister().DetermineNetwork(context.Background()) diff --git a/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES index 01fa0848e5ed..ffb665be9366 100644 --- a/internal/httpclient/.openapi-generator/FILES +++ b/internal/httpclient/.openapi-generator/FILES @@ -15,12 +15,13 @@ docs/ConsistencyRequestParameters.md docs/ContinueWith.md docs/ContinueWithRecoveryUi.md docs/ContinueWithRecoveryUiFlow.md +docs/ContinueWithRedirectBrowserTo.md docs/ContinueWithSetOrySessionToken.md docs/ContinueWithSettingsUi.md docs/ContinueWithSettingsUiFlow.md docs/ContinueWithVerificationUi.md docs/ContinueWithVerificationUiFlow.md -docs/CourierApi.md +docs/CourierAPI.md docs/CourierMessageStatus.md docs/CourierMessageType.md docs/CreateIdentityBody.md @@ -32,15 +33,16 @@ docs/ErrorBrowserLocationChangeRequired.md docs/ErrorFlowReplaced.md docs/ErrorGeneric.md docs/FlowError.md -docs/FrontendApi.md +docs/FrontendAPI.md docs/GenericError.md docs/GetVersion200Response.md docs/HealthNotReadyStatus.md docs/HealthStatus.md docs/Identity.md -docs/IdentityApi.md +docs/IdentityAPI.md docs/IdentityCredentials.md docs/IdentityCredentialsCode.md +docs/IdentityCredentialsCodeAddress.md docs/IdentityCredentialsOidc.md docs/IdentityCredentialsOidcProvider.md docs/IdentityCredentialsPassword.md @@ -61,7 +63,7 @@ docs/LoginFlowState.md docs/LogoutFlow.md docs/Message.md docs/MessageDispatch.md -docs/MetadataApi.md +docs/MetadataAPI.md docs/NeedsPrivilegedSessionError.md docs/OAuth2Client.md docs/OAuth2ConsentRequestOpenIDConnectContext.md @@ -100,8 +102,10 @@ docs/UiText.md docs/UpdateIdentityBody.md docs/UpdateLoginFlowBody.md docs/UpdateLoginFlowWithCodeMethod.md +docs/UpdateLoginFlowWithIdentifierFirstMethod.md docs/UpdateLoginFlowWithLookupSecretMethod.md docs/UpdateLoginFlowWithOidcMethod.md +docs/UpdateLoginFlowWithPasskeyMethod.md docs/UpdateLoginFlowWithPasswordMethod.md docs/UpdateLoginFlowWithTotpMethod.md docs/UpdateLoginFlowWithWebAuthnMethod.md @@ -111,11 +115,14 @@ docs/UpdateRecoveryFlowWithLinkMethod.md docs/UpdateRegistrationFlowBody.md docs/UpdateRegistrationFlowWithCodeMethod.md docs/UpdateRegistrationFlowWithOidcMethod.md +docs/UpdateRegistrationFlowWithPasskeyMethod.md docs/UpdateRegistrationFlowWithPasswordMethod.md +docs/UpdateRegistrationFlowWithProfileMethod.md docs/UpdateRegistrationFlowWithWebAuthnMethod.md docs/UpdateSettingsFlowBody.md docs/UpdateSettingsFlowWithLookupMethod.md docs/UpdateSettingsFlowWithOidcMethod.md +docs/UpdateSettingsFlowWithPasskeyMethod.md docs/UpdateSettingsFlowWithPasswordMethod.md docs/UpdateSettingsFlowWithProfileMethod.md docs/UpdateSettingsFlowWithTotpMethod.md @@ -136,6 +143,7 @@ model_consistency_request_parameters.go model_continue_with.go model_continue_with_recovery_ui.go model_continue_with_recovery_ui_flow.go +model_continue_with_redirect_browser_to.go model_continue_with_set_ory_session_token.go model_continue_with_settings_ui.go model_continue_with_settings_ui_flow.go @@ -159,6 +167,7 @@ model_health_status.go model_identity.go model_identity_credentials.go model_identity_credentials_code.go +model_identity_credentials_code_address.go model_identity_credentials_oidc.go model_identity_credentials_oidc_provider.go model_identity_credentials_password.go @@ -217,8 +226,10 @@ model_ui_text.go model_update_identity_body.go model_update_login_flow_body.go model_update_login_flow_with_code_method.go +model_update_login_flow_with_identifier_first_method.go model_update_login_flow_with_lookup_secret_method.go model_update_login_flow_with_oidc_method.go +model_update_login_flow_with_passkey_method.go model_update_login_flow_with_password_method.go model_update_login_flow_with_totp_method.go model_update_login_flow_with_web_authn_method.go @@ -228,11 +239,14 @@ model_update_recovery_flow_with_link_method.go model_update_registration_flow_body.go model_update_registration_flow_with_code_method.go model_update_registration_flow_with_oidc_method.go +model_update_registration_flow_with_passkey_method.go model_update_registration_flow_with_password_method.go +model_update_registration_flow_with_profile_method.go model_update_registration_flow_with_web_authn_method.go model_update_settings_flow_body.go model_update_settings_flow_with_lookup_method.go model_update_settings_flow_with_oidc_method.go +model_update_settings_flow_with_passkey_method.go model_update_settings_flow_with_password_method.go model_update_settings_flow_with_profile_method.go model_update_settings_flow_with_totp_method.go diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md index b31d69c86354..d0256bfaec2b 100644 --- a/internal/httpclient/README.md +++ b/internal/httpclient/README.md @@ -6,7 +6,7 @@ This is the API specification for Ory Identities with features such as registrat ## Overview This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [OpenAPI-spec](https://www.openapis.org/) from a remote server, you can easily generate an API client. -- API version: +- API version: - Package version: 1.0.0 - Build package: org.openapitools.codegen.languages.GoClientCodegen @@ -79,63 +79,59 @@ All URIs are relative to *http://localhost* Class | Method | HTTP request | Description ------------ | ------------- | ------------- | ------------- -*CourierApi* | [**GetCourierMessage**](docs/CourierApi.md#getcouriermessage) | **Get** /admin/courier/messages/{id} | Get a Message -*CourierApi* | [**ListCourierMessages**](docs/CourierApi.md#listcouriermessages) | **Get** /admin/courier/messages | List Messages -*FrontendApi* | [**CreateBrowserLoginFlow**](docs/FrontendApi.md#createbrowserloginflow) | **Get** /self-service/login/browser | Create Login Flow for Browsers -*FrontendApi* | [**CreateBrowserLogoutFlow**](docs/FrontendApi.md#createbrowserlogoutflow) | **Get** /self-service/logout/browser | Create a Logout URL for Browsers -*FrontendApi* | [**CreateBrowserRecoveryFlow**](docs/FrontendApi.md#createbrowserrecoveryflow) | **Get** /self-service/recovery/browser | Create Recovery Flow for Browsers -*FrontendApi* | [**CreateBrowserRegistrationFlow**](docs/FrontendApi.md#createbrowserregistrationflow) | **Get** /self-service/registration/browser | Create Registration Flow for Browsers -*FrontendApi* | [**CreateBrowserSettingsFlow**](docs/FrontendApi.md#createbrowsersettingsflow) | **Get** /self-service/settings/browser | Create Settings Flow for Browsers -*FrontendApi* | [**CreateBrowserVerificationFlow**](docs/FrontendApi.md#createbrowserverificationflow) | **Get** /self-service/verification/browser | Create Verification Flow for Browser Clients -*FrontendApi* | [**CreateNativeLoginFlow**](docs/FrontendApi.md#createnativeloginflow) | **Get** /self-service/login/api | Create Login Flow for Native Apps -*FrontendApi* | [**CreateNativeRecoveryFlow**](docs/FrontendApi.md#createnativerecoveryflow) | **Get** /self-service/recovery/api | Create Recovery Flow for Native Apps -*FrontendApi* | [**CreateNativeRegistrationFlow**](docs/FrontendApi.md#createnativeregistrationflow) | **Get** /self-service/registration/api | Create Registration Flow for Native Apps -*FrontendApi* | [**CreateNativeSettingsFlow**](docs/FrontendApi.md#createnativesettingsflow) | **Get** /self-service/settings/api | Create Settings Flow for Native Apps -*FrontendApi* | [**CreateNativeVerificationFlow**](docs/FrontendApi.md#createnativeverificationflow) | **Get** /self-service/verification/api | Create Verification Flow for Native Apps -*FrontendApi* | [**DisableMyOtherSessions**](docs/FrontendApi.md#disablemyothersessions) | **Delete** /sessions | Disable my other sessions -*FrontendApi* | [**DisableMySession**](docs/FrontendApi.md#disablemysession) | **Delete** /sessions/{id} | Disable one of my sessions -*FrontendApi* | [**ExchangeSessionToken**](docs/FrontendApi.md#exchangesessiontoken) | **Get** /sessions/token-exchange | Exchange Session Token -*FrontendApi* | [**GetFlowError**](docs/FrontendApi.md#getflowerror) | **Get** /self-service/errors | Get User-Flow Errors -*FrontendApi* | [**GetLoginFlow**](docs/FrontendApi.md#getloginflow) | **Get** /self-service/login/flows | Get Login Flow -*FrontendApi* | [**GetRecoveryFlow**](docs/FrontendApi.md#getrecoveryflow) | **Get** /self-service/recovery/flows | Get Recovery Flow -*FrontendApi* | [**GetRegistrationFlow**](docs/FrontendApi.md#getregistrationflow) | **Get** /self-service/registration/flows | Get Registration Flow -*FrontendApi* | [**GetSettingsFlow**](docs/FrontendApi.md#getsettingsflow) | **Get** /self-service/settings/flows | Get Settings Flow -*FrontendApi* | [**GetVerificationFlow**](docs/FrontendApi.md#getverificationflow) | **Get** /self-service/verification/flows | Get Verification Flow -*FrontendApi* | [**GetWebAuthnJavaScript**](docs/FrontendApi.md#getwebauthnjavascript) | **Get** /.well-known/ory/webauthn.js | Get WebAuthn JavaScript -*FrontendApi* | [**ListMySessions**](docs/FrontendApi.md#listmysessions) | **Get** /sessions | Get My Active Sessions -*FrontendApi* | [**PerformNativeLogout**](docs/FrontendApi.md#performnativelogout) | **Delete** /self-service/logout/api | Perform Logout for Native Apps -*FrontendApi* | [**ToSession**](docs/FrontendApi.md#tosession) | **Get** /sessions/whoami | Check Who the Current HTTP Session Belongs To -*FrontendApi* | [**UpdateLoginFlow**](docs/FrontendApi.md#updateloginflow) | **Post** /self-service/login | Submit a Login Flow -*FrontendApi* | [**UpdateLogoutFlow**](docs/FrontendApi.md#updatelogoutflow) | **Get** /self-service/logout | Update Logout Flow -*FrontendApi* | [**UpdateRecoveryFlow**](docs/FrontendApi.md#updaterecoveryflow) | **Post** /self-service/recovery | Update Recovery Flow -*FrontendApi* | [**UpdateRegistrationFlow**](docs/FrontendApi.md#updateregistrationflow) | **Post** /self-service/registration | Update Registration Flow -*FrontendApi* | [**UpdateSettingsFlow**](docs/FrontendApi.md#updatesettingsflow) | **Post** /self-service/settings | Complete Settings Flow -*FrontendApi* | [**UpdateVerificationFlow**](docs/FrontendApi.md#updateverificationflow) | **Post** /self-service/verification | Complete Verification Flow -*IdentityApi* | [**AdminCurrentSessionExtend**](docs/IdentityApi.md#admincurrentsessionextend) | **Get** /admin/token/extend | Calling this endpoint refreshes a current user session. If `session.refresh_min_time_left` is set it will only refresh the session after this time has passed. -*IdentityApi* | [**AdminIdentitySession**](docs/IdentityApi.md#adminidentitysession) | **Get** /admin/identities/{id}/session | Calling this endpoint issues a session for a given identity. -*IdentityApi* | [**AdminUpdateCredentials**](docs/IdentityApi.md#adminupdatecredentials) | **Put** /identities/{id}/credentials | Update Identity Credentials -*IdentityApi* | [**AdminUpdateIdentityBody**](docs/IdentityApi.md#adminupdateidentitybody) | **Post** /identities/validate | Validates provided traits and state -*IdentityApi* | [**BatchPatchIdentities**](docs/IdentityApi.md#batchpatchidentities) | **Patch** /admin/identities | Create and deletes multiple identities -*IdentityApi* | [**CreateIdentity**](docs/IdentityApi.md#createidentity) | **Post** /admin/identities | Create an Identity -*IdentityApi* | [**CreateRecoveryCodeForIdentity**](docs/IdentityApi.md#createrecoverycodeforidentity) | **Post** /admin/recovery/code | Create a Recovery Code -*IdentityApi* | [**CreateRecoveryLinkForIdentity**](docs/IdentityApi.md#createrecoverylinkforidentity) | **Post** /admin/recovery/link | Create a Recovery Link -*IdentityApi* | [**DeleteIdentity**](docs/IdentityApi.md#deleteidentity) | **Delete** /admin/identities/{id} | Delete an Identity -*IdentityApi* | [**DeleteIdentityCredentials**](docs/IdentityApi.md#deleteidentitycredentials) | **Delete** /admin/identities/{id}/credentials/{type} | Delete a credential for a specific identity -*IdentityApi* | [**DeleteIdentitySessions**](docs/IdentityApi.md#deleteidentitysessions) | **Delete** /admin/identities/{id}/sessions | Delete & Invalidate an Identity's Sessions -*IdentityApi* | [**DisableSession**](docs/IdentityApi.md#disablesession) | **Delete** /admin/sessions/{id} | Deactivate a Session -*IdentityApi* | [**ExtendSession**](docs/IdentityApi.md#extendsession) | **Patch** /admin/sessions/{id}/extend | Extend a Session -*IdentityApi* | [**GetIdentity**](docs/IdentityApi.md#getidentity) | **Get** /admin/identities/{id} | Get an Identity -*IdentityApi* | [**GetIdentitySchema**](docs/IdentityApi.md#getidentityschema) | **Get** /schemas/{id} | Get Identity JSON Schema -*IdentityApi* | [**GetSession**](docs/IdentityApi.md#getsession) | **Get** /admin/sessions/{id} | Get Session -*IdentityApi* | [**ListIdentities**](docs/IdentityApi.md#listidentities) | **Get** /admin/identities | List Identities -*IdentityApi* | [**ListIdentitySchemas**](docs/IdentityApi.md#listidentityschemas) | **Get** /schemas | Get all Identity Schemas -*IdentityApi* | [**ListIdentitySessions**](docs/IdentityApi.md#listidentitysessions) | **Get** /admin/identities/{id}/sessions | List an Identity's Sessions -*IdentityApi* | [**ListSessions**](docs/IdentityApi.md#listsessions) | **Get** /admin/sessions | List All Sessions -*IdentityApi* | [**PatchIdentity**](docs/IdentityApi.md#patchidentity) | **Patch** /admin/identities/{id} | Patch an Identity -*IdentityApi* | [**UpdateIdentity**](docs/IdentityApi.md#updateidentity) | **Put** /admin/identities/{id} | Update an Identity -*MetadataApi* | [**GetVersion**](docs/MetadataApi.md#getversion) | **Get** /version | Return Running Software Version. -*MetadataApi* | [**IsAlive**](docs/MetadataApi.md#isalive) | **Get** /health/alive | Check HTTP Server Status -*MetadataApi* | [**IsReady**](docs/MetadataApi.md#isready) | **Get** /health/ready | Check HTTP Server and Database Status +*CourierAPI* | [**GetCourierMessage**](docs/CourierAPI.md#getcouriermessage) | **Get** /admin/courier/messages/{id} | Get a Message +*CourierAPI* | [**ListCourierMessages**](docs/CourierAPI.md#listcouriermessages) | **Get** /admin/courier/messages | List Messages +*FrontendAPI* | [**CreateBrowserLoginFlow**](docs/FrontendAPI.md#createbrowserloginflow) | **Get** /self-service/login/browser | Create Login Flow for Browsers +*FrontendAPI* | [**CreateBrowserLogoutFlow**](docs/FrontendAPI.md#createbrowserlogoutflow) | **Get** /self-service/logout/browser | Create a Logout URL for Browsers +*FrontendAPI* | [**CreateBrowserRecoveryFlow**](docs/FrontendAPI.md#createbrowserrecoveryflow) | **Get** /self-service/recovery/browser | Create Recovery Flow for Browsers +*FrontendAPI* | [**CreateBrowserRegistrationFlow**](docs/FrontendAPI.md#createbrowserregistrationflow) | **Get** /self-service/registration/browser | Create Registration Flow for Browsers +*FrontendAPI* | [**CreateBrowserSettingsFlow**](docs/FrontendAPI.md#createbrowsersettingsflow) | **Get** /self-service/settings/browser | Create Settings Flow for Browsers +*FrontendAPI* | [**CreateBrowserVerificationFlow**](docs/FrontendAPI.md#createbrowserverificationflow) | **Get** /self-service/verification/browser | Create Verification Flow for Browser Clients +*FrontendAPI* | [**CreateNativeLoginFlow**](docs/FrontendAPI.md#createnativeloginflow) | **Get** /self-service/login/api | Create Login Flow for Native Apps +*FrontendAPI* | [**CreateNativeRecoveryFlow**](docs/FrontendAPI.md#createnativerecoveryflow) | **Get** /self-service/recovery/api | Create Recovery Flow for Native Apps +*FrontendAPI* | [**CreateNativeRegistrationFlow**](docs/FrontendAPI.md#createnativeregistrationflow) | **Get** /self-service/registration/api | Create Registration Flow for Native Apps +*FrontendAPI* | [**CreateNativeSettingsFlow**](docs/FrontendAPI.md#createnativesettingsflow) | **Get** /self-service/settings/api | Create Settings Flow for Native Apps +*FrontendAPI* | [**CreateNativeVerificationFlow**](docs/FrontendAPI.md#createnativeverificationflow) | **Get** /self-service/verification/api | Create Verification Flow for Native Apps +*FrontendAPI* | [**DisableMyOtherSessions**](docs/FrontendAPI.md#disablemyothersessions) | **Delete** /sessions | Disable my other sessions +*FrontendAPI* | [**DisableMySession**](docs/FrontendAPI.md#disablemysession) | **Delete** /sessions/{id} | Disable one of my sessions +*FrontendAPI* | [**ExchangeSessionToken**](docs/FrontendAPI.md#exchangesessiontoken) | **Get** /sessions/token-exchange | Exchange Session Token +*FrontendAPI* | [**GetFlowError**](docs/FrontendAPI.md#getflowerror) | **Get** /self-service/errors | Get User-Flow Errors +*FrontendAPI* | [**GetLoginFlow**](docs/FrontendAPI.md#getloginflow) | **Get** /self-service/login/flows | Get Login Flow +*FrontendAPI* | [**GetRecoveryFlow**](docs/FrontendAPI.md#getrecoveryflow) | **Get** /self-service/recovery/flows | Get Recovery Flow +*FrontendAPI* | [**GetRegistrationFlow**](docs/FrontendAPI.md#getregistrationflow) | **Get** /self-service/registration/flows | Get Registration Flow +*FrontendAPI* | [**GetSettingsFlow**](docs/FrontendAPI.md#getsettingsflow) | **Get** /self-service/settings/flows | Get Settings Flow +*FrontendAPI* | [**GetVerificationFlow**](docs/FrontendAPI.md#getverificationflow) | **Get** /self-service/verification/flows | Get Verification Flow +*FrontendAPI* | [**GetWebAuthnJavaScript**](docs/FrontendAPI.md#getwebauthnjavascript) | **Get** /.well-known/ory/webauthn.js | Get WebAuthn JavaScript +*FrontendAPI* | [**ListMySessions**](docs/FrontendAPI.md#listmysessions) | **Get** /sessions | Get My Active Sessions +*FrontendAPI* | [**PerformNativeLogout**](docs/FrontendAPI.md#performnativelogout) | **Delete** /self-service/logout/api | Perform Logout for Native Apps +*FrontendAPI* | [**ToSession**](docs/FrontendAPI.md#tosession) | **Get** /sessions/whoami | Check Who the Current HTTP Session Belongs To +*FrontendAPI* | [**UpdateLoginFlow**](docs/FrontendAPI.md#updateloginflow) | **Post** /self-service/login | Submit a Login Flow +*FrontendAPI* | [**UpdateLogoutFlow**](docs/FrontendAPI.md#updatelogoutflow) | **Get** /self-service/logout | Update Logout Flow +*FrontendAPI* | [**UpdateRecoveryFlow**](docs/FrontendAPI.md#updaterecoveryflow) | **Post** /self-service/recovery | Update Recovery Flow +*FrontendAPI* | [**UpdateRegistrationFlow**](docs/FrontendAPI.md#updateregistrationflow) | **Post** /self-service/registration | Update Registration Flow +*FrontendAPI* | [**UpdateSettingsFlow**](docs/FrontendAPI.md#updatesettingsflow) | **Post** /self-service/settings | Complete Settings Flow +*FrontendAPI* | [**UpdateVerificationFlow**](docs/FrontendAPI.md#updateverificationflow) | **Post** /self-service/verification | Complete Verification Flow +*IdentityAPI* | [**BatchPatchIdentities**](docs/IdentityAPI.md#batchpatchidentities) | **Patch** /admin/identities | Create multiple identities +*IdentityAPI* | [**CreateIdentity**](docs/IdentityAPI.md#createidentity) | **Post** /admin/identities | Create an Identity +*IdentityAPI* | [**CreateRecoveryCodeForIdentity**](docs/IdentityAPI.md#createrecoverycodeforidentity) | **Post** /admin/recovery/code | Create a Recovery Code +*IdentityAPI* | [**CreateRecoveryLinkForIdentity**](docs/IdentityAPI.md#createrecoverylinkforidentity) | **Post** /admin/recovery/link | Create a Recovery Link +*IdentityAPI* | [**DeleteIdentity**](docs/IdentityAPI.md#deleteidentity) | **Delete** /admin/identities/{id} | Delete an Identity +*IdentityAPI* | [**DeleteIdentityCredentials**](docs/IdentityAPI.md#deleteidentitycredentials) | **Delete** /admin/identities/{id}/credentials/{type} | Delete a credential for a specific identity +*IdentityAPI* | [**DeleteIdentitySessions**](docs/IdentityAPI.md#deleteidentitysessions) | **Delete** /admin/identities/{id}/sessions | Delete & Invalidate an Identity's Sessions +*IdentityAPI* | [**DisableSession**](docs/IdentityAPI.md#disablesession) | **Delete** /admin/sessions/{id} | Deactivate a Session +*IdentityAPI* | [**ExtendSession**](docs/IdentityAPI.md#extendsession) | **Patch** /admin/sessions/{id}/extend | Extend a Session +*IdentityAPI* | [**GetIdentity**](docs/IdentityAPI.md#getidentity) | **Get** /admin/identities/{id} | Get an Identity +*IdentityAPI* | [**GetIdentitySchema**](docs/IdentityAPI.md#getidentityschema) | **Get** /schemas/{id} | Get Identity JSON Schema +*IdentityAPI* | [**GetSession**](docs/IdentityAPI.md#getsession) | **Get** /admin/sessions/{id} | Get Session +*IdentityAPI* | [**ListIdentities**](docs/IdentityAPI.md#listidentities) | **Get** /admin/identities | List Identities +*IdentityAPI* | [**ListIdentitySchemas**](docs/IdentityAPI.md#listidentityschemas) | **Get** /schemas | Get all Identity Schemas +*IdentityAPI* | [**ListIdentitySessions**](docs/IdentityAPI.md#listidentitysessions) | **Get** /admin/identities/{id}/sessions | List an Identity's Sessions +*IdentityAPI* | [**ListSessions**](docs/IdentityAPI.md#listsessions) | **Get** /admin/sessions | List All Sessions +*IdentityAPI* | [**PatchIdentity**](docs/IdentityAPI.md#patchidentity) | **Patch** /admin/identities/{id} | Patch an Identity +*IdentityAPI* | [**UpdateIdentity**](docs/IdentityAPI.md#updateidentity) | **Put** /admin/identities/{id} | Update an Identity +*MetadataAPI* | [**GetVersion**](docs/MetadataAPI.md#getversion) | **Get** /version | Return Running Software Version. +*MetadataAPI* | [**IsAlive**](docs/MetadataAPI.md#isalive) | **Get** /health/alive | Check HTTP Server Status +*MetadataAPI* | [**IsReady**](docs/MetadataAPI.md#isready) | **Get** /health/ready | Check HTTP Server and Database Status ## Documentation For Models @@ -146,6 +142,7 @@ Class | Method | HTTP request | Description - [ContinueWith](docs/ContinueWith.md) - [ContinueWithRecoveryUi](docs/ContinueWithRecoveryUi.md) - [ContinueWithRecoveryUiFlow](docs/ContinueWithRecoveryUiFlow.md) + - [ContinueWithRedirectBrowserTo](docs/ContinueWithRedirectBrowserTo.md) - [ContinueWithSetOrySessionToken](docs/ContinueWithSetOrySessionToken.md) - [ContinueWithSettingsUi](docs/ContinueWithSettingsUi.md) - [ContinueWithSettingsUiFlow](docs/ContinueWithSettingsUiFlow.md) @@ -169,6 +166,7 @@ Class | Method | HTTP request | Description - [Identity](docs/Identity.md) - [IdentityCredentials](docs/IdentityCredentials.md) - [IdentityCredentialsCode](docs/IdentityCredentialsCode.md) + - [IdentityCredentialsCodeAddress](docs/IdentityCredentialsCodeAddress.md) - [IdentityCredentialsOidc](docs/IdentityCredentialsOidc.md) - [IdentityCredentialsOidcProvider](docs/IdentityCredentialsOidcProvider.md) - [IdentityCredentialsPassword](docs/IdentityCredentialsPassword.md) @@ -227,8 +225,10 @@ Class | Method | HTTP request | Description - [UpdateIdentityBody](docs/UpdateIdentityBody.md) - [UpdateLoginFlowBody](docs/UpdateLoginFlowBody.md) - [UpdateLoginFlowWithCodeMethod](docs/UpdateLoginFlowWithCodeMethod.md) + - [UpdateLoginFlowWithIdentifierFirstMethod](docs/UpdateLoginFlowWithIdentifierFirstMethod.md) - [UpdateLoginFlowWithLookupSecretMethod](docs/UpdateLoginFlowWithLookupSecretMethod.md) - [UpdateLoginFlowWithOidcMethod](docs/UpdateLoginFlowWithOidcMethod.md) + - [UpdateLoginFlowWithPasskeyMethod](docs/UpdateLoginFlowWithPasskeyMethod.md) - [UpdateLoginFlowWithPasswordMethod](docs/UpdateLoginFlowWithPasswordMethod.md) - [UpdateLoginFlowWithTotpMethod](docs/UpdateLoginFlowWithTotpMethod.md) - [UpdateLoginFlowWithWebAuthnMethod](docs/UpdateLoginFlowWithWebAuthnMethod.md) @@ -238,11 +238,14 @@ Class | Method | HTTP request | Description - [UpdateRegistrationFlowBody](docs/UpdateRegistrationFlowBody.md) - [UpdateRegistrationFlowWithCodeMethod](docs/UpdateRegistrationFlowWithCodeMethod.md) - [UpdateRegistrationFlowWithOidcMethod](docs/UpdateRegistrationFlowWithOidcMethod.md) + - [UpdateRegistrationFlowWithPasskeyMethod](docs/UpdateRegistrationFlowWithPasskeyMethod.md) - [UpdateRegistrationFlowWithPasswordMethod](docs/UpdateRegistrationFlowWithPasswordMethod.md) + - [UpdateRegistrationFlowWithProfileMethod](docs/UpdateRegistrationFlowWithProfileMethod.md) - [UpdateRegistrationFlowWithWebAuthnMethod](docs/UpdateRegistrationFlowWithWebAuthnMethod.md) - [UpdateSettingsFlowBody](docs/UpdateSettingsFlowBody.md) - [UpdateSettingsFlowWithLookupMethod](docs/UpdateSettingsFlowWithLookupMethod.md) - [UpdateSettingsFlowWithOidcMethod](docs/UpdateSettingsFlowWithOidcMethod.md) + - [UpdateSettingsFlowWithPasskeyMethod](docs/UpdateSettingsFlowWithPasskeyMethod.md) - [UpdateSettingsFlowWithPasswordMethod](docs/UpdateSettingsFlowWithPasswordMethod.md) - [UpdateSettingsFlowWithProfileMethod](docs/UpdateSettingsFlowWithProfileMethod.md) - [UpdateSettingsFlowWithTotpMethod](docs/UpdateSettingsFlowWithTotpMethod.md) diff --git a/internal/httpclient/api_courier.go b/internal/httpclient/api_courier.go index a7e43bcd183e..36f10d0a6281 100644 --- a/internal/httpclient/api_courier.go +++ b/internal/httpclient/api_courier.go @@ -25,48 +25,48 @@ var ( _ context.Context ) -type CourierApi interface { +type CourierAPI interface { /* * GetCourierMessage Get a Message * Gets a specific messages by the given ID. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id MessageID is the ID of the message. - * @return CourierApiApiGetCourierMessageRequest + * @return CourierAPIApiGetCourierMessageRequest */ - GetCourierMessage(ctx context.Context, id string) CourierApiApiGetCourierMessageRequest + GetCourierMessage(ctx context.Context, id string) CourierAPIApiGetCourierMessageRequest /* * GetCourierMessageExecute executes the request * @return Message */ - GetCourierMessageExecute(r CourierApiApiGetCourierMessageRequest) (*Message, *http.Response, error) + GetCourierMessageExecute(r CourierAPIApiGetCourierMessageRequest) (*Message, *http.Response, error) /* * ListCourierMessages List Messages * Lists all messages by given status and recipient. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return CourierApiApiListCourierMessagesRequest + * @return CourierAPIApiListCourierMessagesRequest */ - ListCourierMessages(ctx context.Context) CourierApiApiListCourierMessagesRequest + ListCourierMessages(ctx context.Context) CourierAPIApiListCourierMessagesRequest /* * ListCourierMessagesExecute executes the request * @return []Message */ - ListCourierMessagesExecute(r CourierApiApiListCourierMessagesRequest) ([]Message, *http.Response, error) + ListCourierMessagesExecute(r CourierAPIApiListCourierMessagesRequest) ([]Message, *http.Response, error) } -// CourierApiService CourierApi service -type CourierApiService service +// CourierAPIService CourierAPI service +type CourierAPIService service -type CourierApiApiGetCourierMessageRequest struct { +type CourierAPIApiGetCourierMessageRequest struct { ctx context.Context - ApiService CourierApi + ApiService CourierAPI id string } -func (r CourierApiApiGetCourierMessageRequest) Execute() (*Message, *http.Response, error) { +func (r CourierAPIApiGetCourierMessageRequest) Execute() (*Message, *http.Response, error) { return r.ApiService.GetCourierMessageExecute(r) } @@ -75,10 +75,10 @@ func (r CourierApiApiGetCourierMessageRequest) Execute() (*Message, *http.Respon * Gets a specific messages by the given ID. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id MessageID is the ID of the message. - * @return CourierApiApiGetCourierMessageRequest + * @return CourierAPIApiGetCourierMessageRequest */ -func (a *CourierApiService) GetCourierMessage(ctx context.Context, id string) CourierApiApiGetCourierMessageRequest { - return CourierApiApiGetCourierMessageRequest{ +func (a *CourierAPIService) GetCourierMessage(ctx context.Context, id string) CourierAPIApiGetCourierMessageRequest { + return CourierAPIApiGetCourierMessageRequest{ ApiService: a, ctx: ctx, id: id, @@ -89,7 +89,7 @@ func (a *CourierApiService) GetCourierMessage(ctx context.Context, id string) Co * Execute executes the request * @return Message */ -func (a *CourierApiService) GetCourierMessageExecute(r CourierApiApiGetCourierMessageRequest) (*Message, *http.Response, error) { +func (a *CourierAPIService) GetCourierMessageExecute(r CourierAPIApiGetCourierMessageRequest) (*Message, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -99,7 +99,7 @@ func (a *CourierApiService) GetCourierMessageExecute(r CourierApiApiGetCourierMe localVarReturnValue *Message ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CourierApiService.GetCourierMessage") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CourierAPIService.GetCourierMessage") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -152,7 +152,7 @@ func (a *CourierApiService) GetCourierMessageExecute(r CourierApiApiGetCourierMe return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -196,33 +196,33 @@ func (a *CourierApiService) GetCourierMessageExecute(r CourierApiApiGetCourierMe return localVarReturnValue, localVarHTTPResponse, nil } -type CourierApiApiListCourierMessagesRequest struct { +type CourierAPIApiListCourierMessagesRequest struct { ctx context.Context - ApiService CourierApi + ApiService CourierAPI pageSize *int64 pageToken *string status *CourierMessageStatus recipient *string } -func (r CourierApiApiListCourierMessagesRequest) PageSize(pageSize int64) CourierApiApiListCourierMessagesRequest { +func (r CourierAPIApiListCourierMessagesRequest) PageSize(pageSize int64) CourierAPIApiListCourierMessagesRequest { r.pageSize = &pageSize return r } -func (r CourierApiApiListCourierMessagesRequest) PageToken(pageToken string) CourierApiApiListCourierMessagesRequest { +func (r CourierAPIApiListCourierMessagesRequest) PageToken(pageToken string) CourierAPIApiListCourierMessagesRequest { r.pageToken = &pageToken return r } -func (r CourierApiApiListCourierMessagesRequest) Status(status CourierMessageStatus) CourierApiApiListCourierMessagesRequest { +func (r CourierAPIApiListCourierMessagesRequest) Status(status CourierMessageStatus) CourierAPIApiListCourierMessagesRequest { r.status = &status return r } -func (r CourierApiApiListCourierMessagesRequest) Recipient(recipient string) CourierApiApiListCourierMessagesRequest { +func (r CourierAPIApiListCourierMessagesRequest) Recipient(recipient string) CourierAPIApiListCourierMessagesRequest { r.recipient = &recipient return r } -func (r CourierApiApiListCourierMessagesRequest) Execute() ([]Message, *http.Response, error) { +func (r CourierAPIApiListCourierMessagesRequest) Execute() ([]Message, *http.Response, error) { return r.ApiService.ListCourierMessagesExecute(r) } @@ -230,10 +230,10 @@ func (r CourierApiApiListCourierMessagesRequest) Execute() ([]Message, *http.Res * ListCourierMessages List Messages * Lists all messages by given status and recipient. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return CourierApiApiListCourierMessagesRequest + * @return CourierAPIApiListCourierMessagesRequest */ -func (a *CourierApiService) ListCourierMessages(ctx context.Context) CourierApiApiListCourierMessagesRequest { - return CourierApiApiListCourierMessagesRequest{ +func (a *CourierAPIService) ListCourierMessages(ctx context.Context) CourierAPIApiListCourierMessagesRequest { + return CourierAPIApiListCourierMessagesRequest{ ApiService: a, ctx: ctx, } @@ -243,7 +243,7 @@ func (a *CourierApiService) ListCourierMessages(ctx context.Context) CourierApiA * Execute executes the request * @return []Message */ -func (a *CourierApiService) ListCourierMessagesExecute(r CourierApiApiListCourierMessagesRequest) ([]Message, *http.Response, error) { +func (a *CourierAPIService) ListCourierMessagesExecute(r CourierAPIApiListCourierMessagesRequest) ([]Message, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -253,7 +253,7 @@ func (a *CourierApiService) ListCourierMessagesExecute(r CourierApiApiListCourie localVarReturnValue []Message ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CourierApiService.ListCourierMessages") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CourierAPIService.ListCourierMessages") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -317,7 +317,7 @@ func (a *CourierApiService) ListCourierMessagesExecute(r CourierApiApiListCourie return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { diff --git a/internal/httpclient/api_frontend.go b/internal/httpclient/api_frontend.go index cc5f2531a640..94dc1ad0e152 100644 --- a/internal/httpclient/api_frontend.go +++ b/internal/httpclient/api_frontend.go @@ -25,7 +25,7 @@ var ( _ context.Context ) -type FrontendApi interface { +type FrontendAPI interface { /* * CreateBrowserLoginFlow Create Login Flow for Browsers @@ -53,15 +53,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserLoginFlowRequest + * @return FrontendAPIApiCreateBrowserLoginFlowRequest */ - CreateBrowserLoginFlow(ctx context.Context) FrontendApiApiCreateBrowserLoginFlowRequest + CreateBrowserLoginFlow(ctx context.Context) FrontendAPIApiCreateBrowserLoginFlowRequest /* * CreateBrowserLoginFlowExecute executes the request * @return LoginFlow */ - CreateBrowserLoginFlowExecute(r FrontendApiApiCreateBrowserLoginFlowRequest) (*LoginFlow, *http.Response, error) + CreateBrowserLoginFlowExecute(r FrontendAPIApiCreateBrowserLoginFlowRequest) (*LoginFlow, *http.Response, error) /* * CreateBrowserLogoutFlow Create a Logout URL for Browsers @@ -76,15 +76,15 @@ type FrontendApi interface { When calling this endpoint from a backend, please ensure to properly forward the HTTP cookies. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserLogoutFlowRequest + * @return FrontendAPIApiCreateBrowserLogoutFlowRequest */ - CreateBrowserLogoutFlow(ctx context.Context) FrontendApiApiCreateBrowserLogoutFlowRequest + CreateBrowserLogoutFlow(ctx context.Context) FrontendAPIApiCreateBrowserLogoutFlowRequest /* * CreateBrowserLogoutFlowExecute executes the request * @return LogoutFlow */ - CreateBrowserLogoutFlowExecute(r FrontendApiApiCreateBrowserLogoutFlowRequest) (*LogoutFlow, *http.Response, error) + CreateBrowserLogoutFlowExecute(r FrontendAPIApiCreateBrowserLogoutFlowRequest) (*LogoutFlow, *http.Response, error) /* * CreateBrowserRecoveryFlow Create Recovery Flow for Browsers @@ -99,15 +99,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserRecoveryFlowRequest + * @return FrontendAPIApiCreateBrowserRecoveryFlowRequest */ - CreateBrowserRecoveryFlow(ctx context.Context) FrontendApiApiCreateBrowserRecoveryFlowRequest + CreateBrowserRecoveryFlow(ctx context.Context) FrontendAPIApiCreateBrowserRecoveryFlowRequest /* * CreateBrowserRecoveryFlowExecute executes the request * @return RecoveryFlow */ - CreateBrowserRecoveryFlowExecute(r FrontendApiApiCreateBrowserRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) + CreateBrowserRecoveryFlowExecute(r FrontendAPIApiCreateBrowserRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) /* * CreateBrowserRegistrationFlow Create Registration Flow for Browsers @@ -131,15 +131,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserRegistrationFlowRequest + * @return FrontendAPIApiCreateBrowserRegistrationFlowRequest */ - CreateBrowserRegistrationFlow(ctx context.Context) FrontendApiApiCreateBrowserRegistrationFlowRequest + CreateBrowserRegistrationFlow(ctx context.Context) FrontendAPIApiCreateBrowserRegistrationFlowRequest /* * CreateBrowserRegistrationFlowExecute executes the request * @return RegistrationFlow */ - CreateBrowserRegistrationFlowExecute(r FrontendApiApiCreateBrowserRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) + CreateBrowserRegistrationFlowExecute(r FrontendAPIApiCreateBrowserRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) /* * CreateBrowserSettingsFlow Create Settings Flow for Browsers @@ -170,15 +170,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserSettingsFlowRequest + * @return FrontendAPIApiCreateBrowserSettingsFlowRequest */ - CreateBrowserSettingsFlow(ctx context.Context) FrontendApiApiCreateBrowserSettingsFlowRequest + CreateBrowserSettingsFlow(ctx context.Context) FrontendAPIApiCreateBrowserSettingsFlowRequest /* * CreateBrowserSettingsFlowExecute executes the request * @return SettingsFlow */ - CreateBrowserSettingsFlowExecute(r FrontendApiApiCreateBrowserSettingsFlowRequest) (*SettingsFlow, *http.Response, error) + CreateBrowserSettingsFlowExecute(r FrontendAPIApiCreateBrowserSettingsFlowRequest) (*SettingsFlow, *http.Response, error) /* * CreateBrowserVerificationFlow Create Verification Flow for Browser Clients @@ -191,15 +191,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateBrowserVerificationFlowRequest + * @return FrontendAPIApiCreateBrowserVerificationFlowRequest */ - CreateBrowserVerificationFlow(ctx context.Context) FrontendApiApiCreateBrowserVerificationFlowRequest + CreateBrowserVerificationFlow(ctx context.Context) FrontendAPIApiCreateBrowserVerificationFlowRequest /* * CreateBrowserVerificationFlowExecute executes the request * @return VerificationFlow */ - CreateBrowserVerificationFlowExecute(r FrontendApiApiCreateBrowserVerificationFlowRequest) (*VerificationFlow, *http.Response, error) + CreateBrowserVerificationFlowExecute(r FrontendAPIApiCreateBrowserVerificationFlowRequest) (*VerificationFlow, *http.Response, error) /* * CreateNativeLoginFlow Create Login Flow for Native Apps @@ -224,15 +224,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeLoginFlowRequest + * @return FrontendAPIApiCreateNativeLoginFlowRequest */ - CreateNativeLoginFlow(ctx context.Context) FrontendApiApiCreateNativeLoginFlowRequest + CreateNativeLoginFlow(ctx context.Context) FrontendAPIApiCreateNativeLoginFlowRequest /* * CreateNativeLoginFlowExecute executes the request * @return LoginFlow */ - CreateNativeLoginFlowExecute(r FrontendApiApiCreateNativeLoginFlowRequest) (*LoginFlow, *http.Response, error) + CreateNativeLoginFlowExecute(r FrontendAPIApiCreateNativeLoginFlowRequest) (*LoginFlow, *http.Response, error) /* * CreateNativeRecoveryFlow Create Recovery Flow for Native Apps @@ -250,15 +250,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeRecoveryFlowRequest + * @return FrontendAPIApiCreateNativeRecoveryFlowRequest */ - CreateNativeRecoveryFlow(ctx context.Context) FrontendApiApiCreateNativeRecoveryFlowRequest + CreateNativeRecoveryFlow(ctx context.Context) FrontendAPIApiCreateNativeRecoveryFlowRequest /* * CreateNativeRecoveryFlowExecute executes the request * @return RecoveryFlow */ - CreateNativeRecoveryFlowExecute(r FrontendApiApiCreateNativeRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) + CreateNativeRecoveryFlowExecute(r FrontendAPIApiCreateNativeRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) /* * CreateNativeRegistrationFlow Create Registration Flow for Native Apps @@ -282,15 +282,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeRegistrationFlowRequest + * @return FrontendAPIApiCreateNativeRegistrationFlowRequest */ - CreateNativeRegistrationFlow(ctx context.Context) FrontendApiApiCreateNativeRegistrationFlowRequest + CreateNativeRegistrationFlow(ctx context.Context) FrontendAPIApiCreateNativeRegistrationFlowRequest /* * CreateNativeRegistrationFlowExecute executes the request * @return RegistrationFlow */ - CreateNativeRegistrationFlowExecute(r FrontendApiApiCreateNativeRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) + CreateNativeRegistrationFlowExecute(r FrontendAPIApiCreateNativeRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) /* * CreateNativeSettingsFlow Create Settings Flow for Native Apps @@ -317,15 +317,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeSettingsFlowRequest + * @return FrontendAPIApiCreateNativeSettingsFlowRequest */ - CreateNativeSettingsFlow(ctx context.Context) FrontendApiApiCreateNativeSettingsFlowRequest + CreateNativeSettingsFlow(ctx context.Context) FrontendAPIApiCreateNativeSettingsFlowRequest /* * CreateNativeSettingsFlowExecute executes the request * @return SettingsFlow */ - CreateNativeSettingsFlowExecute(r FrontendApiApiCreateNativeSettingsFlowRequest) (*SettingsFlow, *http.Response, error) + CreateNativeSettingsFlowExecute(r FrontendAPIApiCreateNativeSettingsFlowRequest) (*SettingsFlow, *http.Response, error) /* * CreateNativeVerificationFlow Create Verification Flow for Native Apps @@ -341,30 +341,30 @@ type FrontendApi interface { More information can be found at [Ory Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiCreateNativeVerificationFlowRequest + * @return FrontendAPIApiCreateNativeVerificationFlowRequest */ - CreateNativeVerificationFlow(ctx context.Context) FrontendApiApiCreateNativeVerificationFlowRequest + CreateNativeVerificationFlow(ctx context.Context) FrontendAPIApiCreateNativeVerificationFlowRequest /* * CreateNativeVerificationFlowExecute executes the request * @return VerificationFlow */ - CreateNativeVerificationFlowExecute(r FrontendApiApiCreateNativeVerificationFlowRequest) (*VerificationFlow, *http.Response, error) + CreateNativeVerificationFlowExecute(r FrontendAPIApiCreateNativeVerificationFlowRequest) (*VerificationFlow, *http.Response, error) /* * DisableMyOtherSessions Disable my other sessions * Calling this endpoint invalidates all except the current session that belong to the logged-in user. Session data are not deleted. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiDisableMyOtherSessionsRequest + * @return FrontendAPIApiDisableMyOtherSessionsRequest */ - DisableMyOtherSessions(ctx context.Context) FrontendApiApiDisableMyOtherSessionsRequest + DisableMyOtherSessions(ctx context.Context) FrontendAPIApiDisableMyOtherSessionsRequest /* * DisableMyOtherSessionsExecute executes the request * @return DeleteMySessionsCount */ - DisableMyOtherSessionsExecute(r FrontendApiApiDisableMyOtherSessionsRequest) (*DeleteMySessionsCount, *http.Response, error) + DisableMyOtherSessionsExecute(r FrontendAPIApiDisableMyOtherSessionsRequest) (*DeleteMySessionsCount, *http.Response, error) /* * DisableMySession Disable one of my sessions @@ -372,27 +372,27 @@ type FrontendApi interface { Session data are not deleted. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the session's ID. - * @return FrontendApiApiDisableMySessionRequest + * @return FrontendAPIApiDisableMySessionRequest */ - DisableMySession(ctx context.Context, id string) FrontendApiApiDisableMySessionRequest + DisableMySession(ctx context.Context, id string) FrontendAPIApiDisableMySessionRequest /* * DisableMySessionExecute executes the request */ - DisableMySessionExecute(r FrontendApiApiDisableMySessionRequest) (*http.Response, error) + DisableMySessionExecute(r FrontendAPIApiDisableMySessionRequest) (*http.Response, error) /* * ExchangeSessionToken Exchange Session Token * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiExchangeSessionTokenRequest + * @return FrontendAPIApiExchangeSessionTokenRequest */ - ExchangeSessionToken(ctx context.Context) FrontendApiApiExchangeSessionTokenRequest + ExchangeSessionToken(ctx context.Context) FrontendAPIApiExchangeSessionTokenRequest /* * ExchangeSessionTokenExecute executes the request * @return SuccessfulNativeLogin */ - ExchangeSessionTokenExecute(r FrontendApiApiExchangeSessionTokenRequest) (*SuccessfulNativeLogin, *http.Response, error) + ExchangeSessionTokenExecute(r FrontendAPIApiExchangeSessionTokenRequest) (*SuccessfulNativeLogin, *http.Response, error) /* * GetFlowError Get User-Flow Errors @@ -404,15 +404,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User User Facing Error Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-facing-errors). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetFlowErrorRequest + * @return FrontendAPIApiGetFlowErrorRequest */ - GetFlowError(ctx context.Context) FrontendApiApiGetFlowErrorRequest + GetFlowError(ctx context.Context) FrontendAPIApiGetFlowErrorRequest /* * GetFlowErrorExecute executes the request * @return FlowError */ - GetFlowErrorExecute(r FrontendApiApiGetFlowErrorRequest) (*FlowError, *http.Response, error) + GetFlowErrorExecute(r FrontendAPIApiGetFlowErrorRequest) (*FlowError, *http.Response, error) /* * GetLoginFlow Get Login Flow @@ -440,15 +440,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetLoginFlowRequest + * @return FrontendAPIApiGetLoginFlowRequest */ - GetLoginFlow(ctx context.Context) FrontendApiApiGetLoginFlowRequest + GetLoginFlow(ctx context.Context) FrontendAPIApiGetLoginFlowRequest /* * GetLoginFlowExecute executes the request * @return LoginFlow */ - GetLoginFlowExecute(r FrontendApiApiGetLoginFlowRequest) (*LoginFlow, *http.Response, error) + GetLoginFlowExecute(r FrontendAPIApiGetLoginFlowRequest) (*LoginFlow, *http.Response, error) /* * GetRecoveryFlow Get Recovery Flow @@ -471,15 +471,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetRecoveryFlowRequest + * @return FrontendAPIApiGetRecoveryFlowRequest */ - GetRecoveryFlow(ctx context.Context) FrontendApiApiGetRecoveryFlowRequest + GetRecoveryFlow(ctx context.Context) FrontendAPIApiGetRecoveryFlowRequest /* * GetRecoveryFlowExecute executes the request * @return RecoveryFlow */ - GetRecoveryFlowExecute(r FrontendApiApiGetRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) + GetRecoveryFlowExecute(r FrontendAPIApiGetRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) /* * GetRegistrationFlow Get Registration Flow @@ -507,15 +507,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetRegistrationFlowRequest + * @return FrontendAPIApiGetRegistrationFlowRequest */ - GetRegistrationFlow(ctx context.Context) FrontendApiApiGetRegistrationFlowRequest + GetRegistrationFlow(ctx context.Context) FrontendAPIApiGetRegistrationFlowRequest /* * GetRegistrationFlowExecute executes the request * @return RegistrationFlow */ - GetRegistrationFlowExecute(r FrontendApiApiGetRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) + GetRegistrationFlowExecute(r FrontendAPIApiGetRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) /* * GetSettingsFlow Get Settings Flow @@ -539,15 +539,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetSettingsFlowRequest + * @return FrontendAPIApiGetSettingsFlowRequest */ - GetSettingsFlow(ctx context.Context) FrontendApiApiGetSettingsFlowRequest + GetSettingsFlow(ctx context.Context) FrontendAPIApiGetSettingsFlowRequest /* * GetSettingsFlowExecute executes the request * @return SettingsFlow */ - GetSettingsFlowExecute(r FrontendApiApiGetSettingsFlowRequest) (*SettingsFlow, *http.Response, error) + GetSettingsFlowExecute(r FrontendAPIApiGetSettingsFlowRequest) (*SettingsFlow, *http.Response, error) /* * GetVerificationFlow Get Verification Flow @@ -570,15 +570,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetVerificationFlowRequest + * @return FrontendAPIApiGetVerificationFlowRequest */ - GetVerificationFlow(ctx context.Context) FrontendApiApiGetVerificationFlowRequest + GetVerificationFlow(ctx context.Context) FrontendAPIApiGetVerificationFlowRequest /* * GetVerificationFlowExecute executes the request * @return VerificationFlow */ - GetVerificationFlowExecute(r FrontendApiApiGetVerificationFlowRequest) (*VerificationFlow, *http.Response, error) + GetVerificationFlowExecute(r FrontendAPIApiGetVerificationFlowRequest) (*VerificationFlow, *http.Response, error) /* * GetWebAuthnJavaScript Get WebAuthn JavaScript @@ -592,30 +592,30 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiGetWebAuthnJavaScriptRequest + * @return FrontendAPIApiGetWebAuthnJavaScriptRequest */ - GetWebAuthnJavaScript(ctx context.Context) FrontendApiApiGetWebAuthnJavaScriptRequest + GetWebAuthnJavaScript(ctx context.Context) FrontendAPIApiGetWebAuthnJavaScriptRequest /* * GetWebAuthnJavaScriptExecute executes the request * @return string */ - GetWebAuthnJavaScriptExecute(r FrontendApiApiGetWebAuthnJavaScriptRequest) (string, *http.Response, error) + GetWebAuthnJavaScriptExecute(r FrontendAPIApiGetWebAuthnJavaScriptRequest) (string, *http.Response, error) /* * ListMySessions Get My Active Sessions * This endpoints returns all other active sessions that belong to the logged-in user. The current session can be retrieved by calling the `/sessions/whoami` endpoint. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiListMySessionsRequest + * @return FrontendAPIApiListMySessionsRequest */ - ListMySessions(ctx context.Context) FrontendApiApiListMySessionsRequest + ListMySessions(ctx context.Context) FrontendAPIApiListMySessionsRequest /* * ListMySessionsExecute executes the request * @return []Session */ - ListMySessionsExecute(r FrontendApiApiListMySessionsRequest) ([]Session, *http.Response, error) + ListMySessionsExecute(r FrontendAPIApiListMySessionsRequest) ([]Session, *http.Response, error) /* * PerformNativeLogout Perform Logout for Native Apps @@ -628,14 +628,14 @@ type FrontendApi interface { This endpoint does not remove any HTTP Cookies - use the Browser-Based Self-Service Logout Flow instead. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiPerformNativeLogoutRequest + * @return FrontendAPIApiPerformNativeLogoutRequest */ - PerformNativeLogout(ctx context.Context) FrontendApiApiPerformNativeLogoutRequest + PerformNativeLogout(ctx context.Context) FrontendAPIApiPerformNativeLogoutRequest /* * PerformNativeLogoutExecute executes the request */ - PerformNativeLogoutExecute(r FrontendApiApiPerformNativeLogoutRequest) (*http.Response, error) + PerformNativeLogoutExecute(r FrontendAPIApiPerformNativeLogoutRequest) (*http.Response, error) /* * ToSession Check Who the Current HTTP Session Belongs To @@ -700,15 +700,15 @@ type FrontendApi interface { `session_inactive`: No active session was found in the request (e.g. no Ory Session Cookie / Ory Session Token). `session_aal2_required`: An active session was found but it does not fulfil the Authenticator Assurance Level, implying that the session must (e.g.) authenticate the second factor. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiToSessionRequest + * @return FrontendAPIApiToSessionRequest */ - ToSession(ctx context.Context) FrontendApiApiToSessionRequest + ToSession(ctx context.Context) FrontendAPIApiToSessionRequest /* * ToSessionExecute executes the request * @return Session */ - ToSessionExecute(r FrontendApiApiToSessionRequest) (*Session, *http.Response, error) + ToSessionExecute(r FrontendAPIApiToSessionRequest) (*Session, *http.Response, error) /* * UpdateLoginFlow Submit a Login Flow @@ -740,15 +740,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiUpdateLoginFlowRequest + * @return FrontendAPIApiUpdateLoginFlowRequest */ - UpdateLoginFlow(ctx context.Context) FrontendApiApiUpdateLoginFlowRequest + UpdateLoginFlow(ctx context.Context) FrontendAPIApiUpdateLoginFlowRequest /* * UpdateLoginFlowExecute executes the request * @return SuccessfulNativeLogin */ - UpdateLoginFlowExecute(r FrontendApiApiUpdateLoginFlowRequest) (*SuccessfulNativeLogin, *http.Response, error) + UpdateLoginFlowExecute(r FrontendAPIApiUpdateLoginFlowRequest) (*SuccessfulNativeLogin, *http.Response, error) /* * UpdateLogoutFlow Update Logout Flow @@ -766,14 +766,14 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Logout Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-logout). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiUpdateLogoutFlowRequest + * @return FrontendAPIApiUpdateLogoutFlowRequest */ - UpdateLogoutFlow(ctx context.Context) FrontendApiApiUpdateLogoutFlowRequest + UpdateLogoutFlow(ctx context.Context) FrontendAPIApiUpdateLogoutFlowRequest /* * UpdateLogoutFlowExecute executes the request */ - UpdateLogoutFlowExecute(r FrontendApiApiUpdateLogoutFlowRequest) (*http.Response, error) + UpdateLogoutFlowExecute(r FrontendAPIApiUpdateLogoutFlowRequest) (*http.Response, error) /* * UpdateRecoveryFlow Update Recovery Flow @@ -794,15 +794,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiUpdateRecoveryFlowRequest + * @return FrontendAPIApiUpdateRecoveryFlowRequest */ - UpdateRecoveryFlow(ctx context.Context) FrontendApiApiUpdateRecoveryFlowRequest + UpdateRecoveryFlow(ctx context.Context) FrontendAPIApiUpdateRecoveryFlowRequest /* * UpdateRecoveryFlowExecute executes the request * @return RecoveryFlow */ - UpdateRecoveryFlowExecute(r FrontendApiApiUpdateRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) + UpdateRecoveryFlowExecute(r FrontendAPIApiUpdateRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) /* * UpdateRegistrationFlow Update Registration Flow @@ -835,15 +835,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiUpdateRegistrationFlowRequest + * @return FrontendAPIApiUpdateRegistrationFlowRequest */ - UpdateRegistrationFlow(ctx context.Context) FrontendApiApiUpdateRegistrationFlowRequest + UpdateRegistrationFlow(ctx context.Context) FrontendAPIApiUpdateRegistrationFlowRequest /* * UpdateRegistrationFlowExecute executes the request * @return SuccessfulNativeRegistration */ - UpdateRegistrationFlowExecute(r FrontendApiApiUpdateRegistrationFlowRequest) (*SuccessfulNativeRegistration, *http.Response, error) + UpdateRegistrationFlowExecute(r FrontendAPIApiUpdateRegistrationFlowRequest) (*SuccessfulNativeRegistration, *http.Response, error) /* * UpdateSettingsFlow Complete Settings Flow @@ -891,15 +891,15 @@ type FrontendApi interface { More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiUpdateSettingsFlowRequest + * @return FrontendAPIApiUpdateSettingsFlowRequest */ - UpdateSettingsFlow(ctx context.Context) FrontendApiApiUpdateSettingsFlowRequest + UpdateSettingsFlow(ctx context.Context) FrontendAPIApiUpdateSettingsFlowRequest /* * UpdateSettingsFlowExecute executes the request * @return SettingsFlow */ - UpdateSettingsFlowExecute(r FrontendApiApiUpdateSettingsFlowRequest) (*SettingsFlow, *http.Response, error) + UpdateSettingsFlowExecute(r FrontendAPIApiUpdateSettingsFlowRequest) (*SettingsFlow, *http.Response, error) /* * UpdateVerificationFlow Complete Verification Flow @@ -920,57 +920,62 @@ type FrontendApi interface { More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiUpdateVerificationFlowRequest + * @return FrontendAPIApiUpdateVerificationFlowRequest */ - UpdateVerificationFlow(ctx context.Context) FrontendApiApiUpdateVerificationFlowRequest + UpdateVerificationFlow(ctx context.Context) FrontendAPIApiUpdateVerificationFlowRequest /* * UpdateVerificationFlowExecute executes the request * @return VerificationFlow */ - UpdateVerificationFlowExecute(r FrontendApiApiUpdateVerificationFlowRequest) (*VerificationFlow, *http.Response, error) + UpdateVerificationFlowExecute(r FrontendAPIApiUpdateVerificationFlowRequest) (*VerificationFlow, *http.Response, error) } -// FrontendApiService FrontendApi service -type FrontendApiService service +// FrontendAPIService FrontendAPI service +type FrontendAPIService service -type FrontendApiApiCreateBrowserLoginFlowRequest struct { +type FrontendAPIApiCreateBrowserLoginFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI refresh *bool aal *string returnTo *string cookie *string loginChallenge *string organization *string + via *string } -func (r FrontendApiApiCreateBrowserLoginFlowRequest) Refresh(refresh bool) FrontendApiApiCreateBrowserLoginFlowRequest { +func (r FrontendAPIApiCreateBrowserLoginFlowRequest) Refresh(refresh bool) FrontendAPIApiCreateBrowserLoginFlowRequest { r.refresh = &refresh return r } -func (r FrontendApiApiCreateBrowserLoginFlowRequest) Aal(aal string) FrontendApiApiCreateBrowserLoginFlowRequest { +func (r FrontendAPIApiCreateBrowserLoginFlowRequest) Aal(aal string) FrontendAPIApiCreateBrowserLoginFlowRequest { r.aal = &aal return r } -func (r FrontendApiApiCreateBrowserLoginFlowRequest) ReturnTo(returnTo string) FrontendApiApiCreateBrowserLoginFlowRequest { +func (r FrontendAPIApiCreateBrowserLoginFlowRequest) ReturnTo(returnTo string) FrontendAPIApiCreateBrowserLoginFlowRequest { r.returnTo = &returnTo return r } -func (r FrontendApiApiCreateBrowserLoginFlowRequest) Cookie(cookie string) FrontendApiApiCreateBrowserLoginFlowRequest { +func (r FrontendAPIApiCreateBrowserLoginFlowRequest) Cookie(cookie string) FrontendAPIApiCreateBrowserLoginFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiCreateBrowserLoginFlowRequest) LoginChallenge(loginChallenge string) FrontendApiApiCreateBrowserLoginFlowRequest { +func (r FrontendAPIApiCreateBrowserLoginFlowRequest) LoginChallenge(loginChallenge string) FrontendAPIApiCreateBrowserLoginFlowRequest { r.loginChallenge = &loginChallenge return r } -func (r FrontendApiApiCreateBrowserLoginFlowRequest) Organization(organization string) FrontendApiApiCreateBrowserLoginFlowRequest { +func (r FrontendAPIApiCreateBrowserLoginFlowRequest) Organization(organization string) FrontendAPIApiCreateBrowserLoginFlowRequest { r.organization = &organization return r } +func (r FrontendAPIApiCreateBrowserLoginFlowRequest) Via(via string) FrontendAPIApiCreateBrowserLoginFlowRequest { + r.via = &via + return r +} -func (r FrontendApiApiCreateBrowserLoginFlowRequest) Execute() (*LoginFlow, *http.Response, error) { +func (r FrontendAPIApiCreateBrowserLoginFlowRequest) Execute() (*LoginFlow, *http.Response, error) { return r.ApiService.CreateBrowserLoginFlowExecute(r) } @@ -1001,10 +1006,10 @@ This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Fi More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateBrowserLoginFlowRequest + - @return FrontendAPIApiCreateBrowserLoginFlowRequest */ -func (a *FrontendApiService) CreateBrowserLoginFlow(ctx context.Context) FrontendApiApiCreateBrowserLoginFlowRequest { - return FrontendApiApiCreateBrowserLoginFlowRequest{ +func (a *FrontendAPIService) CreateBrowserLoginFlow(ctx context.Context) FrontendAPIApiCreateBrowserLoginFlowRequest { + return FrontendAPIApiCreateBrowserLoginFlowRequest{ ApiService: a, ctx: ctx, } @@ -1014,7 +1019,7 @@ func (a *FrontendApiService) CreateBrowserLoginFlow(ctx context.Context) Fronten * Execute executes the request * @return LoginFlow */ -func (a *FrontendApiService) CreateBrowserLoginFlowExecute(r FrontendApiApiCreateBrowserLoginFlowRequest) (*LoginFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateBrowserLoginFlowExecute(r FrontendAPIApiCreateBrowserLoginFlowRequest) (*LoginFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -1024,7 +1029,7 @@ func (a *FrontendApiService) CreateBrowserLoginFlowExecute(r FrontendApiApiCreat localVarReturnValue *LoginFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateBrowserLoginFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateBrowserLoginFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1050,6 +1055,9 @@ func (a *FrontendApiService) CreateBrowserLoginFlowExecute(r FrontendApiApiCreat if r.organization != nil { localVarQueryParams.Add("organization", parameterToString(*r.organization, "")) } + if r.via != nil { + localVarQueryParams.Add("via", parameterToString(*r.via, "")) + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} @@ -1080,7 +1088,7 @@ func (a *FrontendApiService) CreateBrowserLoginFlowExecute(r FrontendApiApiCreat return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1124,23 +1132,23 @@ func (a *FrontendApiService) CreateBrowserLoginFlowExecute(r FrontendApiApiCreat return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateBrowserLogoutFlowRequest struct { +type FrontendAPIApiCreateBrowserLogoutFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI cookie *string returnTo *string } -func (r FrontendApiApiCreateBrowserLogoutFlowRequest) Cookie(cookie string) FrontendApiApiCreateBrowserLogoutFlowRequest { +func (r FrontendAPIApiCreateBrowserLogoutFlowRequest) Cookie(cookie string) FrontendAPIApiCreateBrowserLogoutFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiCreateBrowserLogoutFlowRequest) ReturnTo(returnTo string) FrontendApiApiCreateBrowserLogoutFlowRequest { +func (r FrontendAPIApiCreateBrowserLogoutFlowRequest) ReturnTo(returnTo string) FrontendAPIApiCreateBrowserLogoutFlowRequest { r.returnTo = &returnTo return r } -func (r FrontendApiApiCreateBrowserLogoutFlowRequest) Execute() (*LogoutFlow, *http.Response, error) { +func (r FrontendAPIApiCreateBrowserLogoutFlowRequest) Execute() (*LogoutFlow, *http.Response, error) { return r.ApiService.CreateBrowserLogoutFlowExecute(r) } @@ -1157,10 +1165,10 @@ a 401 error. When calling this endpoint from a backend, please ensure to properly forward the HTTP cookies. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateBrowserLogoutFlowRequest + - @return FrontendAPIApiCreateBrowserLogoutFlowRequest */ -func (a *FrontendApiService) CreateBrowserLogoutFlow(ctx context.Context) FrontendApiApiCreateBrowserLogoutFlowRequest { - return FrontendApiApiCreateBrowserLogoutFlowRequest{ +func (a *FrontendAPIService) CreateBrowserLogoutFlow(ctx context.Context) FrontendAPIApiCreateBrowserLogoutFlowRequest { + return FrontendAPIApiCreateBrowserLogoutFlowRequest{ ApiService: a, ctx: ctx, } @@ -1170,7 +1178,7 @@ func (a *FrontendApiService) CreateBrowserLogoutFlow(ctx context.Context) Fronte * Execute executes the request * @return LogoutFlow */ -func (a *FrontendApiService) CreateBrowserLogoutFlowExecute(r FrontendApiApiCreateBrowserLogoutFlowRequest) (*LogoutFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateBrowserLogoutFlowExecute(r FrontendAPIApiCreateBrowserLogoutFlowRequest) (*LogoutFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -1180,7 +1188,7 @@ func (a *FrontendApiService) CreateBrowserLogoutFlowExecute(r FrontendApiApiCrea localVarReturnValue *LogoutFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateBrowserLogoutFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateBrowserLogoutFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1224,7 +1232,7 @@ func (a *FrontendApiService) CreateBrowserLogoutFlowExecute(r FrontendApiApiCrea return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1280,18 +1288,18 @@ func (a *FrontendApiService) CreateBrowserLogoutFlowExecute(r FrontendApiApiCrea return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateBrowserRecoveryFlowRequest struct { +type FrontendAPIApiCreateBrowserRecoveryFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI returnTo *string } -func (r FrontendApiApiCreateBrowserRecoveryFlowRequest) ReturnTo(returnTo string) FrontendApiApiCreateBrowserRecoveryFlowRequest { +func (r FrontendAPIApiCreateBrowserRecoveryFlowRequest) ReturnTo(returnTo string) FrontendAPIApiCreateBrowserRecoveryFlowRequest { r.returnTo = &returnTo return r } -func (r FrontendApiApiCreateBrowserRecoveryFlowRequest) Execute() (*RecoveryFlow, *http.Response, error) { +func (r FrontendAPIApiCreateBrowserRecoveryFlowRequest) Execute() (*RecoveryFlow, *http.Response, error) { return r.ApiService.CreateBrowserRecoveryFlowExecute(r) } @@ -1309,10 +1317,10 @@ This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Fi More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateBrowserRecoveryFlowRequest + - @return FrontendAPIApiCreateBrowserRecoveryFlowRequest */ -func (a *FrontendApiService) CreateBrowserRecoveryFlow(ctx context.Context) FrontendApiApiCreateBrowserRecoveryFlowRequest { - return FrontendApiApiCreateBrowserRecoveryFlowRequest{ +func (a *FrontendAPIService) CreateBrowserRecoveryFlow(ctx context.Context) FrontendAPIApiCreateBrowserRecoveryFlowRequest { + return FrontendAPIApiCreateBrowserRecoveryFlowRequest{ ApiService: a, ctx: ctx, } @@ -1322,7 +1330,7 @@ func (a *FrontendApiService) CreateBrowserRecoveryFlow(ctx context.Context) Fron * Execute executes the request * @return RecoveryFlow */ -func (a *FrontendApiService) CreateBrowserRecoveryFlowExecute(r FrontendApiApiCreateBrowserRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateBrowserRecoveryFlowExecute(r FrontendAPIApiCreateBrowserRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -1332,7 +1340,7 @@ func (a *FrontendApiService) CreateBrowserRecoveryFlowExecute(r FrontendApiApiCr localVarReturnValue *RecoveryFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateBrowserRecoveryFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateBrowserRecoveryFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1373,7 +1381,7 @@ func (a *FrontendApiService) CreateBrowserRecoveryFlowExecute(r FrontendApiApiCr return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1417,33 +1425,33 @@ func (a *FrontendApiService) CreateBrowserRecoveryFlowExecute(r FrontendApiApiCr return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateBrowserRegistrationFlowRequest struct { +type FrontendAPIApiCreateBrowserRegistrationFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI returnTo *string loginChallenge *string afterVerificationReturnTo *string organization *string } -func (r FrontendApiApiCreateBrowserRegistrationFlowRequest) ReturnTo(returnTo string) FrontendApiApiCreateBrowserRegistrationFlowRequest { +func (r FrontendAPIApiCreateBrowserRegistrationFlowRequest) ReturnTo(returnTo string) FrontendAPIApiCreateBrowserRegistrationFlowRequest { r.returnTo = &returnTo return r } -func (r FrontendApiApiCreateBrowserRegistrationFlowRequest) LoginChallenge(loginChallenge string) FrontendApiApiCreateBrowserRegistrationFlowRequest { +func (r FrontendAPIApiCreateBrowserRegistrationFlowRequest) LoginChallenge(loginChallenge string) FrontendAPIApiCreateBrowserRegistrationFlowRequest { r.loginChallenge = &loginChallenge return r } -func (r FrontendApiApiCreateBrowserRegistrationFlowRequest) AfterVerificationReturnTo(afterVerificationReturnTo string) FrontendApiApiCreateBrowserRegistrationFlowRequest { +func (r FrontendAPIApiCreateBrowserRegistrationFlowRequest) AfterVerificationReturnTo(afterVerificationReturnTo string) FrontendAPIApiCreateBrowserRegistrationFlowRequest { r.afterVerificationReturnTo = &afterVerificationReturnTo return r } -func (r FrontendApiApiCreateBrowserRegistrationFlowRequest) Organization(organization string) FrontendApiApiCreateBrowserRegistrationFlowRequest { +func (r FrontendAPIApiCreateBrowserRegistrationFlowRequest) Organization(organization string) FrontendAPIApiCreateBrowserRegistrationFlowRequest { r.organization = &organization return r } -func (r FrontendApiApiCreateBrowserRegistrationFlowRequest) Execute() (*RegistrationFlow, *http.Response, error) { +func (r FrontendAPIApiCreateBrowserRegistrationFlowRequest) Execute() (*RegistrationFlow, *http.Response, error) { return r.ApiService.CreateBrowserRegistrationFlowExecute(r) } @@ -1470,10 +1478,10 @@ This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Fi More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateBrowserRegistrationFlowRequest + - @return FrontendAPIApiCreateBrowserRegistrationFlowRequest */ -func (a *FrontendApiService) CreateBrowserRegistrationFlow(ctx context.Context) FrontendApiApiCreateBrowserRegistrationFlowRequest { - return FrontendApiApiCreateBrowserRegistrationFlowRequest{ +func (a *FrontendAPIService) CreateBrowserRegistrationFlow(ctx context.Context) FrontendAPIApiCreateBrowserRegistrationFlowRequest { + return FrontendAPIApiCreateBrowserRegistrationFlowRequest{ ApiService: a, ctx: ctx, } @@ -1483,7 +1491,7 @@ func (a *FrontendApiService) CreateBrowserRegistrationFlow(ctx context.Context) * Execute executes the request * @return RegistrationFlow */ -func (a *FrontendApiService) CreateBrowserRegistrationFlowExecute(r FrontendApiApiCreateBrowserRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateBrowserRegistrationFlowExecute(r FrontendAPIApiCreateBrowserRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -1493,7 +1501,7 @@ func (a *FrontendApiService) CreateBrowserRegistrationFlowExecute(r FrontendApiA localVarReturnValue *RegistrationFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateBrowserRegistrationFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateBrowserRegistrationFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1543,7 +1551,7 @@ func (a *FrontendApiService) CreateBrowserRegistrationFlowExecute(r FrontendApiA return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1577,23 +1585,23 @@ func (a *FrontendApiService) CreateBrowserRegistrationFlowExecute(r FrontendApiA return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateBrowserSettingsFlowRequest struct { +type FrontendAPIApiCreateBrowserSettingsFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI returnTo *string cookie *string } -func (r FrontendApiApiCreateBrowserSettingsFlowRequest) ReturnTo(returnTo string) FrontendApiApiCreateBrowserSettingsFlowRequest { +func (r FrontendAPIApiCreateBrowserSettingsFlowRequest) ReturnTo(returnTo string) FrontendAPIApiCreateBrowserSettingsFlowRequest { r.returnTo = &returnTo return r } -func (r FrontendApiApiCreateBrowserSettingsFlowRequest) Cookie(cookie string) FrontendApiApiCreateBrowserSettingsFlowRequest { +func (r FrontendAPIApiCreateBrowserSettingsFlowRequest) Cookie(cookie string) FrontendAPIApiCreateBrowserSettingsFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiCreateBrowserSettingsFlowRequest) Execute() (*SettingsFlow, *http.Response, error) { +func (r FrontendAPIApiCreateBrowserSettingsFlowRequest) Execute() (*SettingsFlow, *http.Response, error) { return r.ApiService.CreateBrowserSettingsFlowExecute(r) } @@ -1627,10 +1635,10 @@ This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Fi More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateBrowserSettingsFlowRequest + - @return FrontendAPIApiCreateBrowserSettingsFlowRequest */ -func (a *FrontendApiService) CreateBrowserSettingsFlow(ctx context.Context) FrontendApiApiCreateBrowserSettingsFlowRequest { - return FrontendApiApiCreateBrowserSettingsFlowRequest{ +func (a *FrontendAPIService) CreateBrowserSettingsFlow(ctx context.Context) FrontendAPIApiCreateBrowserSettingsFlowRequest { + return FrontendAPIApiCreateBrowserSettingsFlowRequest{ ApiService: a, ctx: ctx, } @@ -1640,7 +1648,7 @@ func (a *FrontendApiService) CreateBrowserSettingsFlow(ctx context.Context) Fron * Execute executes the request * @return SettingsFlow */ -func (a *FrontendApiService) CreateBrowserSettingsFlowExecute(r FrontendApiApiCreateBrowserSettingsFlowRequest) (*SettingsFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateBrowserSettingsFlowExecute(r FrontendAPIApiCreateBrowserSettingsFlowRequest) (*SettingsFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -1650,7 +1658,7 @@ func (a *FrontendApiService) CreateBrowserSettingsFlowExecute(r FrontendApiApiCr localVarReturnValue *SettingsFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateBrowserSettingsFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateBrowserSettingsFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1694,7 +1702,7 @@ func (a *FrontendApiService) CreateBrowserSettingsFlowExecute(r FrontendApiApiCr return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1758,18 +1766,18 @@ func (a *FrontendApiService) CreateBrowserSettingsFlowExecute(r FrontendApiApiCr return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateBrowserVerificationFlowRequest struct { +type FrontendAPIApiCreateBrowserVerificationFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI returnTo *string } -func (r FrontendApiApiCreateBrowserVerificationFlowRequest) ReturnTo(returnTo string) FrontendApiApiCreateBrowserVerificationFlowRequest { +func (r FrontendAPIApiCreateBrowserVerificationFlowRequest) ReturnTo(returnTo string) FrontendAPIApiCreateBrowserVerificationFlowRequest { r.returnTo = &returnTo return r } -func (r FrontendApiApiCreateBrowserVerificationFlowRequest) Execute() (*VerificationFlow, *http.Response, error) { +func (r FrontendAPIApiCreateBrowserVerificationFlowRequest) Execute() (*VerificationFlow, *http.Response, error) { return r.ApiService.CreateBrowserVerificationFlowExecute(r) } @@ -1785,10 +1793,10 @@ This endpoint is NOT INTENDED for API clients and only works with browsers (Chro More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateBrowserVerificationFlowRequest + - @return FrontendAPIApiCreateBrowserVerificationFlowRequest */ -func (a *FrontendApiService) CreateBrowserVerificationFlow(ctx context.Context) FrontendApiApiCreateBrowserVerificationFlowRequest { - return FrontendApiApiCreateBrowserVerificationFlowRequest{ +func (a *FrontendAPIService) CreateBrowserVerificationFlow(ctx context.Context) FrontendAPIApiCreateBrowserVerificationFlowRequest { + return FrontendAPIApiCreateBrowserVerificationFlowRequest{ ApiService: a, ctx: ctx, } @@ -1798,7 +1806,7 @@ func (a *FrontendApiService) CreateBrowserVerificationFlow(ctx context.Context) * Execute executes the request * @return VerificationFlow */ -func (a *FrontendApiService) CreateBrowserVerificationFlowExecute(r FrontendApiApiCreateBrowserVerificationFlowRequest) (*VerificationFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateBrowserVerificationFlowExecute(r FrontendAPIApiCreateBrowserVerificationFlowRequest) (*VerificationFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -1808,7 +1816,7 @@ func (a *FrontendApiService) CreateBrowserVerificationFlowExecute(r FrontendApiA localVarReturnValue *VerificationFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateBrowserVerificationFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateBrowserVerificationFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1849,7 +1857,7 @@ func (a *FrontendApiService) CreateBrowserVerificationFlowExecute(r FrontendApiA return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1883,9 +1891,9 @@ func (a *FrontendApiService) CreateBrowserVerificationFlowExecute(r FrontendApiA return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateNativeLoginFlowRequest struct { +type FrontendAPIApiCreateNativeLoginFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI refresh *bool aal *string xSessionToken *string @@ -1894,32 +1902,32 @@ type FrontendApiApiCreateNativeLoginFlowRequest struct { via *string } -func (r FrontendApiApiCreateNativeLoginFlowRequest) Refresh(refresh bool) FrontendApiApiCreateNativeLoginFlowRequest { +func (r FrontendAPIApiCreateNativeLoginFlowRequest) Refresh(refresh bool) FrontendAPIApiCreateNativeLoginFlowRequest { r.refresh = &refresh return r } -func (r FrontendApiApiCreateNativeLoginFlowRequest) Aal(aal string) FrontendApiApiCreateNativeLoginFlowRequest { +func (r FrontendAPIApiCreateNativeLoginFlowRequest) Aal(aal string) FrontendAPIApiCreateNativeLoginFlowRequest { r.aal = &aal return r } -func (r FrontendApiApiCreateNativeLoginFlowRequest) XSessionToken(xSessionToken string) FrontendApiApiCreateNativeLoginFlowRequest { +func (r FrontendAPIApiCreateNativeLoginFlowRequest) XSessionToken(xSessionToken string) FrontendAPIApiCreateNativeLoginFlowRequest { r.xSessionToken = &xSessionToken return r } -func (r FrontendApiApiCreateNativeLoginFlowRequest) ReturnSessionTokenExchangeCode(returnSessionTokenExchangeCode bool) FrontendApiApiCreateNativeLoginFlowRequest { +func (r FrontendAPIApiCreateNativeLoginFlowRequest) ReturnSessionTokenExchangeCode(returnSessionTokenExchangeCode bool) FrontendAPIApiCreateNativeLoginFlowRequest { r.returnSessionTokenExchangeCode = &returnSessionTokenExchangeCode return r } -func (r FrontendApiApiCreateNativeLoginFlowRequest) ReturnTo(returnTo string) FrontendApiApiCreateNativeLoginFlowRequest { +func (r FrontendAPIApiCreateNativeLoginFlowRequest) ReturnTo(returnTo string) FrontendAPIApiCreateNativeLoginFlowRequest { r.returnTo = &returnTo return r } -func (r FrontendApiApiCreateNativeLoginFlowRequest) Via(via string) FrontendApiApiCreateNativeLoginFlowRequest { +func (r FrontendAPIApiCreateNativeLoginFlowRequest) Via(via string) FrontendAPIApiCreateNativeLoginFlowRequest { r.via = &via return r } -func (r FrontendApiApiCreateNativeLoginFlowRequest) Execute() (*LoginFlow, *http.Response, error) { +func (r FrontendAPIApiCreateNativeLoginFlowRequest) Execute() (*LoginFlow, *http.Response, error) { return r.ApiService.CreateNativeLoginFlowExecute(r) } @@ -1946,10 +1954,10 @@ This endpoint MUST ONLY be used in scenarios such as native mobile apps (React N More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateNativeLoginFlowRequest + - @return FrontendAPIApiCreateNativeLoginFlowRequest */ -func (a *FrontendApiService) CreateNativeLoginFlow(ctx context.Context) FrontendApiApiCreateNativeLoginFlowRequest { - return FrontendApiApiCreateNativeLoginFlowRequest{ +func (a *FrontendAPIService) CreateNativeLoginFlow(ctx context.Context) FrontendAPIApiCreateNativeLoginFlowRequest { + return FrontendAPIApiCreateNativeLoginFlowRequest{ ApiService: a, ctx: ctx, } @@ -1959,7 +1967,7 @@ func (a *FrontendApiService) CreateNativeLoginFlow(ctx context.Context) Frontend * Execute executes the request * @return LoginFlow */ -func (a *FrontendApiService) CreateNativeLoginFlowExecute(r FrontendApiApiCreateNativeLoginFlowRequest) (*LoginFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateNativeLoginFlowExecute(r FrontendAPIApiCreateNativeLoginFlowRequest) (*LoginFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -1969,7 +1977,7 @@ func (a *FrontendApiService) CreateNativeLoginFlowExecute(r FrontendApiApiCreate localVarReturnValue *LoginFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateNativeLoginFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateNativeLoginFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2025,7 +2033,7 @@ func (a *FrontendApiService) CreateNativeLoginFlowExecute(r FrontendApiApiCreate return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2069,12 +2077,12 @@ func (a *FrontendApiService) CreateNativeLoginFlowExecute(r FrontendApiApiCreate return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateNativeRecoveryFlowRequest struct { +type FrontendAPIApiCreateNativeRecoveryFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI } -func (r FrontendApiApiCreateNativeRecoveryFlowRequest) Execute() (*RecoveryFlow, *http.Response, error) { +func (r FrontendAPIApiCreateNativeRecoveryFlowRequest) Execute() (*RecoveryFlow, *http.Response, error) { return r.ApiService.CreateNativeRecoveryFlowExecute(r) } @@ -2094,10 +2102,10 @@ This endpoint MUST ONLY be used in scenarios such as native mobile apps (React N More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateNativeRecoveryFlowRequest + - @return FrontendAPIApiCreateNativeRecoveryFlowRequest */ -func (a *FrontendApiService) CreateNativeRecoveryFlow(ctx context.Context) FrontendApiApiCreateNativeRecoveryFlowRequest { - return FrontendApiApiCreateNativeRecoveryFlowRequest{ +func (a *FrontendAPIService) CreateNativeRecoveryFlow(ctx context.Context) FrontendAPIApiCreateNativeRecoveryFlowRequest { + return FrontendAPIApiCreateNativeRecoveryFlowRequest{ ApiService: a, ctx: ctx, } @@ -2107,7 +2115,7 @@ func (a *FrontendApiService) CreateNativeRecoveryFlow(ctx context.Context) Front * Execute executes the request * @return RecoveryFlow */ -func (a *FrontendApiService) CreateNativeRecoveryFlowExecute(r FrontendApiApiCreateNativeRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateNativeRecoveryFlowExecute(r FrontendAPIApiCreateNativeRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2117,7 +2125,7 @@ func (a *FrontendApiService) CreateNativeRecoveryFlowExecute(r FrontendApiApiCre localVarReturnValue *RecoveryFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateNativeRecoveryFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateNativeRecoveryFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2155,7 +2163,7 @@ func (a *FrontendApiService) CreateNativeRecoveryFlowExecute(r FrontendApiApiCre return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2199,23 +2207,23 @@ func (a *FrontendApiService) CreateNativeRecoveryFlowExecute(r FrontendApiApiCre return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateNativeRegistrationFlowRequest struct { +type FrontendAPIApiCreateNativeRegistrationFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI returnSessionTokenExchangeCode *bool returnTo *string } -func (r FrontendApiApiCreateNativeRegistrationFlowRequest) ReturnSessionTokenExchangeCode(returnSessionTokenExchangeCode bool) FrontendApiApiCreateNativeRegistrationFlowRequest { +func (r FrontendAPIApiCreateNativeRegistrationFlowRequest) ReturnSessionTokenExchangeCode(returnSessionTokenExchangeCode bool) FrontendAPIApiCreateNativeRegistrationFlowRequest { r.returnSessionTokenExchangeCode = &returnSessionTokenExchangeCode return r } -func (r FrontendApiApiCreateNativeRegistrationFlowRequest) ReturnTo(returnTo string) FrontendApiApiCreateNativeRegistrationFlowRequest { +func (r FrontendAPIApiCreateNativeRegistrationFlowRequest) ReturnTo(returnTo string) FrontendAPIApiCreateNativeRegistrationFlowRequest { r.returnTo = &returnTo return r } -func (r FrontendApiApiCreateNativeRegistrationFlowRequest) Execute() (*RegistrationFlow, *http.Response, error) { +func (r FrontendAPIApiCreateNativeRegistrationFlowRequest) Execute() (*RegistrationFlow, *http.Response, error) { return r.ApiService.CreateNativeRegistrationFlowExecute(r) } @@ -2241,10 +2249,10 @@ This endpoint MUST ONLY be used in scenarios such as native mobile apps (React N More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateNativeRegistrationFlowRequest + - @return FrontendAPIApiCreateNativeRegistrationFlowRequest */ -func (a *FrontendApiService) CreateNativeRegistrationFlow(ctx context.Context) FrontendApiApiCreateNativeRegistrationFlowRequest { - return FrontendApiApiCreateNativeRegistrationFlowRequest{ +func (a *FrontendAPIService) CreateNativeRegistrationFlow(ctx context.Context) FrontendAPIApiCreateNativeRegistrationFlowRequest { + return FrontendAPIApiCreateNativeRegistrationFlowRequest{ ApiService: a, ctx: ctx, } @@ -2254,7 +2262,7 @@ func (a *FrontendApiService) CreateNativeRegistrationFlow(ctx context.Context) F * Execute executes the request * @return RegistrationFlow */ -func (a *FrontendApiService) CreateNativeRegistrationFlowExecute(r FrontendApiApiCreateNativeRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateNativeRegistrationFlowExecute(r FrontendAPIApiCreateNativeRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2264,7 +2272,7 @@ func (a *FrontendApiService) CreateNativeRegistrationFlowExecute(r FrontendApiAp localVarReturnValue *RegistrationFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateNativeRegistrationFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateNativeRegistrationFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2308,7 +2316,7 @@ func (a *FrontendApiService) CreateNativeRegistrationFlowExecute(r FrontendApiAp return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2352,18 +2360,18 @@ func (a *FrontendApiService) CreateNativeRegistrationFlowExecute(r FrontendApiAp return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateNativeSettingsFlowRequest struct { +type FrontendAPIApiCreateNativeSettingsFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI xSessionToken *string } -func (r FrontendApiApiCreateNativeSettingsFlowRequest) XSessionToken(xSessionToken string) FrontendApiApiCreateNativeSettingsFlowRequest { +func (r FrontendAPIApiCreateNativeSettingsFlowRequest) XSessionToken(xSessionToken string) FrontendAPIApiCreateNativeSettingsFlowRequest { r.xSessionToken = &xSessionToken return r } -func (r FrontendApiApiCreateNativeSettingsFlowRequest) Execute() (*SettingsFlow, *http.Response, error) { +func (r FrontendAPIApiCreateNativeSettingsFlowRequest) Execute() (*SettingsFlow, *http.Response, error) { return r.ApiService.CreateNativeSettingsFlowExecute(r) } @@ -2393,10 +2401,10 @@ This endpoint MUST ONLY be used in scenarios such as native mobile apps (React N More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateNativeSettingsFlowRequest + - @return FrontendAPIApiCreateNativeSettingsFlowRequest */ -func (a *FrontendApiService) CreateNativeSettingsFlow(ctx context.Context) FrontendApiApiCreateNativeSettingsFlowRequest { - return FrontendApiApiCreateNativeSettingsFlowRequest{ +func (a *FrontendAPIService) CreateNativeSettingsFlow(ctx context.Context) FrontendAPIApiCreateNativeSettingsFlowRequest { + return FrontendAPIApiCreateNativeSettingsFlowRequest{ ApiService: a, ctx: ctx, } @@ -2406,7 +2414,7 @@ func (a *FrontendApiService) CreateNativeSettingsFlow(ctx context.Context) Front * Execute executes the request * @return SettingsFlow */ -func (a *FrontendApiService) CreateNativeSettingsFlowExecute(r FrontendApiApiCreateNativeSettingsFlowRequest) (*SettingsFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateNativeSettingsFlowExecute(r FrontendAPIApiCreateNativeSettingsFlowRequest) (*SettingsFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2416,7 +2424,7 @@ func (a *FrontendApiService) CreateNativeSettingsFlowExecute(r FrontendApiApiCre localVarReturnValue *SettingsFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateNativeSettingsFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateNativeSettingsFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2457,7 +2465,7 @@ func (a *FrontendApiService) CreateNativeSettingsFlowExecute(r FrontendApiApiCre return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2501,12 +2509,12 @@ func (a *FrontendApiService) CreateNativeSettingsFlowExecute(r FrontendApiApiCre return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiCreateNativeVerificationFlowRequest struct { +type FrontendAPIApiCreateNativeVerificationFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI } -func (r FrontendApiApiCreateNativeVerificationFlowRequest) Execute() (*VerificationFlow, *http.Response, error) { +func (r FrontendAPIApiCreateNativeVerificationFlowRequest) Execute() (*VerificationFlow, *http.Response, error) { return r.ApiService.CreateNativeVerificationFlowExecute(r) } @@ -2524,10 +2532,10 @@ This endpoint MUST ONLY be used in scenarios such as native mobile apps (React N More information can be found at [Ory Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiCreateNativeVerificationFlowRequest + - @return FrontendAPIApiCreateNativeVerificationFlowRequest */ -func (a *FrontendApiService) CreateNativeVerificationFlow(ctx context.Context) FrontendApiApiCreateNativeVerificationFlowRequest { - return FrontendApiApiCreateNativeVerificationFlowRequest{ +func (a *FrontendAPIService) CreateNativeVerificationFlow(ctx context.Context) FrontendAPIApiCreateNativeVerificationFlowRequest { + return FrontendAPIApiCreateNativeVerificationFlowRequest{ ApiService: a, ctx: ctx, } @@ -2537,7 +2545,7 @@ func (a *FrontendApiService) CreateNativeVerificationFlow(ctx context.Context) F * Execute executes the request * @return VerificationFlow */ -func (a *FrontendApiService) CreateNativeVerificationFlowExecute(r FrontendApiApiCreateNativeVerificationFlowRequest) (*VerificationFlow, *http.Response, error) { +func (a *FrontendAPIService) CreateNativeVerificationFlowExecute(r FrontendAPIApiCreateNativeVerificationFlowRequest) (*VerificationFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2547,7 +2555,7 @@ func (a *FrontendApiService) CreateNativeVerificationFlowExecute(r FrontendApiAp localVarReturnValue *VerificationFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.CreateNativeVerificationFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.CreateNativeVerificationFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2585,7 +2593,7 @@ func (a *FrontendApiService) CreateNativeVerificationFlowExecute(r FrontendApiAp return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2629,23 +2637,23 @@ func (a *FrontendApiService) CreateNativeVerificationFlowExecute(r FrontendApiAp return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiDisableMyOtherSessionsRequest struct { +type FrontendAPIApiDisableMyOtherSessionsRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI xSessionToken *string cookie *string } -func (r FrontendApiApiDisableMyOtherSessionsRequest) XSessionToken(xSessionToken string) FrontendApiApiDisableMyOtherSessionsRequest { +func (r FrontendAPIApiDisableMyOtherSessionsRequest) XSessionToken(xSessionToken string) FrontendAPIApiDisableMyOtherSessionsRequest { r.xSessionToken = &xSessionToken return r } -func (r FrontendApiApiDisableMyOtherSessionsRequest) Cookie(cookie string) FrontendApiApiDisableMyOtherSessionsRequest { +func (r FrontendAPIApiDisableMyOtherSessionsRequest) Cookie(cookie string) FrontendAPIApiDisableMyOtherSessionsRequest { r.cookie = &cookie return r } -func (r FrontendApiApiDisableMyOtherSessionsRequest) Execute() (*DeleteMySessionsCount, *http.Response, error) { +func (r FrontendAPIApiDisableMyOtherSessionsRequest) Execute() (*DeleteMySessionsCount, *http.Response, error) { return r.ApiService.DisableMyOtherSessionsExecute(r) } @@ -2655,10 +2663,10 @@ func (r FrontendApiApiDisableMyOtherSessionsRequest) Execute() (*DeleteMySession Session data are not deleted. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiDisableMyOtherSessionsRequest + - @return FrontendAPIApiDisableMyOtherSessionsRequest */ -func (a *FrontendApiService) DisableMyOtherSessions(ctx context.Context) FrontendApiApiDisableMyOtherSessionsRequest { - return FrontendApiApiDisableMyOtherSessionsRequest{ +func (a *FrontendAPIService) DisableMyOtherSessions(ctx context.Context) FrontendAPIApiDisableMyOtherSessionsRequest { + return FrontendAPIApiDisableMyOtherSessionsRequest{ ApiService: a, ctx: ctx, } @@ -2668,7 +2676,7 @@ func (a *FrontendApiService) DisableMyOtherSessions(ctx context.Context) Fronten * Execute executes the request * @return DeleteMySessionsCount */ -func (a *FrontendApiService) DisableMyOtherSessionsExecute(r FrontendApiApiDisableMyOtherSessionsRequest) (*DeleteMySessionsCount, *http.Response, error) { +func (a *FrontendAPIService) DisableMyOtherSessionsExecute(r FrontendAPIApiDisableMyOtherSessionsRequest) (*DeleteMySessionsCount, *http.Response, error) { var ( localVarHTTPMethod = http.MethodDelete localVarPostBody interface{} @@ -2678,7 +2686,7 @@ func (a *FrontendApiService) DisableMyOtherSessionsExecute(r FrontendApiApiDisab localVarReturnValue *DeleteMySessionsCount ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.DisableMyOtherSessions") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.DisableMyOtherSessions") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2722,7 +2730,7 @@ func (a *FrontendApiService) DisableMyOtherSessionsExecute(r FrontendApiApiDisab return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2776,24 +2784,24 @@ func (a *FrontendApiService) DisableMyOtherSessionsExecute(r FrontendApiApiDisab return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiDisableMySessionRequest struct { +type FrontendAPIApiDisableMySessionRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI id string xSessionToken *string cookie *string } -func (r FrontendApiApiDisableMySessionRequest) XSessionToken(xSessionToken string) FrontendApiApiDisableMySessionRequest { +func (r FrontendAPIApiDisableMySessionRequest) XSessionToken(xSessionToken string) FrontendAPIApiDisableMySessionRequest { r.xSessionToken = &xSessionToken return r } -func (r FrontendApiApiDisableMySessionRequest) Cookie(cookie string) FrontendApiApiDisableMySessionRequest { +func (r FrontendAPIApiDisableMySessionRequest) Cookie(cookie string) FrontendAPIApiDisableMySessionRequest { r.cookie = &cookie return r } -func (r FrontendApiApiDisableMySessionRequest) Execute() (*http.Response, error) { +func (r FrontendAPIApiDisableMySessionRequest) Execute() (*http.Response, error) { return r.ApiService.DisableMySessionExecute(r) } @@ -2804,10 +2812,10 @@ func (r FrontendApiApiDisableMySessionRequest) Execute() (*http.Response, error) Session data are not deleted. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the session's ID. - - @return FrontendApiApiDisableMySessionRequest + - @return FrontendAPIApiDisableMySessionRequest */ -func (a *FrontendApiService) DisableMySession(ctx context.Context, id string) FrontendApiApiDisableMySessionRequest { - return FrontendApiApiDisableMySessionRequest{ +func (a *FrontendAPIService) DisableMySession(ctx context.Context, id string) FrontendAPIApiDisableMySessionRequest { + return FrontendAPIApiDisableMySessionRequest{ ApiService: a, ctx: ctx, id: id, @@ -2817,7 +2825,7 @@ func (a *FrontendApiService) DisableMySession(ctx context.Context, id string) Fr /* * Execute executes the request */ -func (a *FrontendApiService) DisableMySessionExecute(r FrontendApiApiDisableMySessionRequest) (*http.Response, error) { +func (a *FrontendAPIService) DisableMySessionExecute(r FrontendAPIApiDisableMySessionRequest) (*http.Response, error) { var ( localVarHTTPMethod = http.MethodDelete localVarPostBody interface{} @@ -2826,7 +2834,7 @@ func (a *FrontendApiService) DisableMySessionExecute(r FrontendApiApiDisableMySe localVarFileBytes []byte ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.DisableMySession") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.DisableMySession") if err != nil { return nil, &GenericOpenAPIError{error: err.Error()} } @@ -2871,7 +2879,7 @@ func (a *FrontendApiService) DisableMySessionExecute(r FrontendApiApiDisableMySe return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2916,33 +2924,33 @@ func (a *FrontendApiService) DisableMySessionExecute(r FrontendApiApiDisableMySe return localVarHTTPResponse, nil } -type FrontendApiApiExchangeSessionTokenRequest struct { +type FrontendAPIApiExchangeSessionTokenRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI initCode *string returnToCode *string } -func (r FrontendApiApiExchangeSessionTokenRequest) InitCode(initCode string) FrontendApiApiExchangeSessionTokenRequest { +func (r FrontendAPIApiExchangeSessionTokenRequest) InitCode(initCode string) FrontendAPIApiExchangeSessionTokenRequest { r.initCode = &initCode return r } -func (r FrontendApiApiExchangeSessionTokenRequest) ReturnToCode(returnToCode string) FrontendApiApiExchangeSessionTokenRequest { +func (r FrontendAPIApiExchangeSessionTokenRequest) ReturnToCode(returnToCode string) FrontendAPIApiExchangeSessionTokenRequest { r.returnToCode = &returnToCode return r } -func (r FrontendApiApiExchangeSessionTokenRequest) Execute() (*SuccessfulNativeLogin, *http.Response, error) { +func (r FrontendAPIApiExchangeSessionTokenRequest) Execute() (*SuccessfulNativeLogin, *http.Response, error) { return r.ApiService.ExchangeSessionTokenExecute(r) } /* * ExchangeSessionToken Exchange Session Token * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return FrontendApiApiExchangeSessionTokenRequest + * @return FrontendAPIApiExchangeSessionTokenRequest */ -func (a *FrontendApiService) ExchangeSessionToken(ctx context.Context) FrontendApiApiExchangeSessionTokenRequest { - return FrontendApiApiExchangeSessionTokenRequest{ +func (a *FrontendAPIService) ExchangeSessionToken(ctx context.Context) FrontendAPIApiExchangeSessionTokenRequest { + return FrontendAPIApiExchangeSessionTokenRequest{ ApiService: a, ctx: ctx, } @@ -2952,7 +2960,7 @@ func (a *FrontendApiService) ExchangeSessionToken(ctx context.Context) FrontendA * Execute executes the request * @return SuccessfulNativeLogin */ -func (a *FrontendApiService) ExchangeSessionTokenExecute(r FrontendApiApiExchangeSessionTokenRequest) (*SuccessfulNativeLogin, *http.Response, error) { +func (a *FrontendAPIService) ExchangeSessionTokenExecute(r FrontendAPIApiExchangeSessionTokenRequest) (*SuccessfulNativeLogin, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2962,7 +2970,7 @@ func (a *FrontendApiService) ExchangeSessionTokenExecute(r FrontendApiApiExchang localVarReturnValue *SuccessfulNativeLogin ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.ExchangeSessionToken") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.ExchangeSessionToken") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3008,7 +3016,7 @@ func (a *FrontendApiService) ExchangeSessionTokenExecute(r FrontendApiApiExchang return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3072,18 +3080,18 @@ func (a *FrontendApiService) ExchangeSessionTokenExecute(r FrontendApiApiExchang return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiGetFlowErrorRequest struct { +type FrontendAPIApiGetFlowErrorRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI id *string } -func (r FrontendApiApiGetFlowErrorRequest) Id(id string) FrontendApiApiGetFlowErrorRequest { +func (r FrontendAPIApiGetFlowErrorRequest) Id(id string) FrontendAPIApiGetFlowErrorRequest { r.id = &id return r } -func (r FrontendApiApiGetFlowErrorRequest) Execute() (*FlowError, *http.Response, error) { +func (r FrontendAPIApiGetFlowErrorRequest) Execute() (*FlowError, *http.Response, error) { return r.ApiService.GetFlowErrorExecute(r) } @@ -3097,10 +3105,10 @@ This endpoint supports stub values to help you implement the error UI: More information can be found at [Ory Kratos User User Facing Error Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-facing-errors). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiGetFlowErrorRequest + - @return FrontendAPIApiGetFlowErrorRequest */ -func (a *FrontendApiService) GetFlowError(ctx context.Context) FrontendApiApiGetFlowErrorRequest { - return FrontendApiApiGetFlowErrorRequest{ +func (a *FrontendAPIService) GetFlowError(ctx context.Context) FrontendAPIApiGetFlowErrorRequest { + return FrontendAPIApiGetFlowErrorRequest{ ApiService: a, ctx: ctx, } @@ -3110,7 +3118,7 @@ func (a *FrontendApiService) GetFlowError(ctx context.Context) FrontendApiApiGet * Execute executes the request * @return FlowError */ -func (a *FrontendApiService) GetFlowErrorExecute(r FrontendApiApiGetFlowErrorRequest) (*FlowError, *http.Response, error) { +func (a *FrontendAPIService) GetFlowErrorExecute(r FrontendAPIApiGetFlowErrorRequest) (*FlowError, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -3120,7 +3128,7 @@ func (a *FrontendApiService) GetFlowErrorExecute(r FrontendApiApiGetFlowErrorReq localVarReturnValue *FlowError ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.GetFlowError") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.GetFlowError") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3162,7 +3170,7 @@ func (a *FrontendApiService) GetFlowErrorExecute(r FrontendApiApiGetFlowErrorReq return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3218,23 +3226,23 @@ func (a *FrontendApiService) GetFlowErrorExecute(r FrontendApiApiGetFlowErrorReq return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiGetLoginFlowRequest struct { +type FrontendAPIApiGetLoginFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI id *string cookie *string } -func (r FrontendApiApiGetLoginFlowRequest) Id(id string) FrontendApiApiGetLoginFlowRequest { +func (r FrontendAPIApiGetLoginFlowRequest) Id(id string) FrontendAPIApiGetLoginFlowRequest { r.id = &id return r } -func (r FrontendApiApiGetLoginFlowRequest) Cookie(cookie string) FrontendApiApiGetLoginFlowRequest { +func (r FrontendAPIApiGetLoginFlowRequest) Cookie(cookie string) FrontendAPIApiGetLoginFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiGetLoginFlowRequest) Execute() (*LoginFlow, *http.Response, error) { +func (r FrontendAPIApiGetLoginFlowRequest) Execute() (*LoginFlow, *http.Response, error) { return r.ApiService.GetLoginFlowExecute(r) } @@ -3264,10 +3272,10 @@ This request may fail due to several reasons. The `error.id` can be one of: More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiGetLoginFlowRequest + - @return FrontendAPIApiGetLoginFlowRequest */ -func (a *FrontendApiService) GetLoginFlow(ctx context.Context) FrontendApiApiGetLoginFlowRequest { - return FrontendApiApiGetLoginFlowRequest{ +func (a *FrontendAPIService) GetLoginFlow(ctx context.Context) FrontendAPIApiGetLoginFlowRequest { + return FrontendAPIApiGetLoginFlowRequest{ ApiService: a, ctx: ctx, } @@ -3277,7 +3285,7 @@ func (a *FrontendApiService) GetLoginFlow(ctx context.Context) FrontendApiApiGet * Execute executes the request * @return LoginFlow */ -func (a *FrontendApiService) GetLoginFlowExecute(r FrontendApiApiGetLoginFlowRequest) (*LoginFlow, *http.Response, error) { +func (a *FrontendAPIService) GetLoginFlowExecute(r FrontendAPIApiGetLoginFlowRequest) (*LoginFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -3287,7 +3295,7 @@ func (a *FrontendApiService) GetLoginFlowExecute(r FrontendApiApiGetLoginFlowReq localVarReturnValue *LoginFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.GetLoginFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.GetLoginFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3332,7 +3340,7 @@ func (a *FrontendApiService) GetLoginFlowExecute(r FrontendApiApiGetLoginFlowReq return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3396,23 +3404,23 @@ func (a *FrontendApiService) GetLoginFlowExecute(r FrontendApiApiGetLoginFlowReq return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiGetRecoveryFlowRequest struct { +type FrontendAPIApiGetRecoveryFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI id *string cookie *string } -func (r FrontendApiApiGetRecoveryFlowRequest) Id(id string) FrontendApiApiGetRecoveryFlowRequest { +func (r FrontendAPIApiGetRecoveryFlowRequest) Id(id string) FrontendAPIApiGetRecoveryFlowRequest { r.id = &id return r } -func (r FrontendApiApiGetRecoveryFlowRequest) Cookie(cookie string) FrontendApiApiGetRecoveryFlowRequest { +func (r FrontendAPIApiGetRecoveryFlowRequest) Cookie(cookie string) FrontendAPIApiGetRecoveryFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiGetRecoveryFlowRequest) Execute() (*RecoveryFlow, *http.Response, error) { +func (r FrontendAPIApiGetRecoveryFlowRequest) Execute() (*RecoveryFlow, *http.Response, error) { return r.ApiService.GetRecoveryFlowExecute(r) } @@ -3437,10 +3445,10 @@ res.render('recovery', flow) More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiGetRecoveryFlowRequest + - @return FrontendAPIApiGetRecoveryFlowRequest */ -func (a *FrontendApiService) GetRecoveryFlow(ctx context.Context) FrontendApiApiGetRecoveryFlowRequest { - return FrontendApiApiGetRecoveryFlowRequest{ +func (a *FrontendAPIService) GetRecoveryFlow(ctx context.Context) FrontendAPIApiGetRecoveryFlowRequest { + return FrontendAPIApiGetRecoveryFlowRequest{ ApiService: a, ctx: ctx, } @@ -3450,7 +3458,7 @@ func (a *FrontendApiService) GetRecoveryFlow(ctx context.Context) FrontendApiApi * Execute executes the request * @return RecoveryFlow */ -func (a *FrontendApiService) GetRecoveryFlowExecute(r FrontendApiApiGetRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) { +func (a *FrontendAPIService) GetRecoveryFlowExecute(r FrontendAPIApiGetRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -3460,7 +3468,7 @@ func (a *FrontendApiService) GetRecoveryFlowExecute(r FrontendApiApiGetRecoveryF localVarReturnValue *RecoveryFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.GetRecoveryFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.GetRecoveryFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3505,7 +3513,7 @@ func (a *FrontendApiService) GetRecoveryFlowExecute(r FrontendApiApiGetRecoveryF return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3559,23 +3567,23 @@ func (a *FrontendApiService) GetRecoveryFlowExecute(r FrontendApiApiGetRecoveryF return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiGetRegistrationFlowRequest struct { +type FrontendAPIApiGetRegistrationFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI id *string cookie *string } -func (r FrontendApiApiGetRegistrationFlowRequest) Id(id string) FrontendApiApiGetRegistrationFlowRequest { +func (r FrontendAPIApiGetRegistrationFlowRequest) Id(id string) FrontendAPIApiGetRegistrationFlowRequest { r.id = &id return r } -func (r FrontendApiApiGetRegistrationFlowRequest) Cookie(cookie string) FrontendApiApiGetRegistrationFlowRequest { +func (r FrontendAPIApiGetRegistrationFlowRequest) Cookie(cookie string) FrontendAPIApiGetRegistrationFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiGetRegistrationFlowRequest) Execute() (*RegistrationFlow, *http.Response, error) { +func (r FrontendAPIApiGetRegistrationFlowRequest) Execute() (*RegistrationFlow, *http.Response, error) { return r.ApiService.GetRegistrationFlowExecute(r) } @@ -3605,10 +3613,10 @@ This request may fail due to several reasons. The `error.id` can be one of: More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiGetRegistrationFlowRequest + - @return FrontendAPIApiGetRegistrationFlowRequest */ -func (a *FrontendApiService) GetRegistrationFlow(ctx context.Context) FrontendApiApiGetRegistrationFlowRequest { - return FrontendApiApiGetRegistrationFlowRequest{ +func (a *FrontendAPIService) GetRegistrationFlow(ctx context.Context) FrontendAPIApiGetRegistrationFlowRequest { + return FrontendAPIApiGetRegistrationFlowRequest{ ApiService: a, ctx: ctx, } @@ -3618,7 +3626,7 @@ func (a *FrontendApiService) GetRegistrationFlow(ctx context.Context) FrontendAp * Execute executes the request * @return RegistrationFlow */ -func (a *FrontendApiService) GetRegistrationFlowExecute(r FrontendApiApiGetRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) { +func (a *FrontendAPIService) GetRegistrationFlowExecute(r FrontendAPIApiGetRegistrationFlowRequest) (*RegistrationFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -3628,7 +3636,7 @@ func (a *FrontendApiService) GetRegistrationFlowExecute(r FrontendApiApiGetRegis localVarReturnValue *RegistrationFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.GetRegistrationFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.GetRegistrationFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3673,7 +3681,7 @@ func (a *FrontendApiService) GetRegistrationFlowExecute(r FrontendApiApiGetRegis return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3737,28 +3745,28 @@ func (a *FrontendApiService) GetRegistrationFlowExecute(r FrontendApiApiGetRegis return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiGetSettingsFlowRequest struct { +type FrontendAPIApiGetSettingsFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI id *string xSessionToken *string cookie *string } -func (r FrontendApiApiGetSettingsFlowRequest) Id(id string) FrontendApiApiGetSettingsFlowRequest { +func (r FrontendAPIApiGetSettingsFlowRequest) Id(id string) FrontendAPIApiGetSettingsFlowRequest { r.id = &id return r } -func (r FrontendApiApiGetSettingsFlowRequest) XSessionToken(xSessionToken string) FrontendApiApiGetSettingsFlowRequest { +func (r FrontendAPIApiGetSettingsFlowRequest) XSessionToken(xSessionToken string) FrontendAPIApiGetSettingsFlowRequest { r.xSessionToken = &xSessionToken return r } -func (r FrontendApiApiGetSettingsFlowRequest) Cookie(cookie string) FrontendApiApiGetSettingsFlowRequest { +func (r FrontendAPIApiGetSettingsFlowRequest) Cookie(cookie string) FrontendAPIApiGetSettingsFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiGetSettingsFlowRequest) Execute() (*SettingsFlow, *http.Response, error) { +func (r FrontendAPIApiGetSettingsFlowRequest) Execute() (*SettingsFlow, *http.Response, error) { return r.ApiService.GetSettingsFlowExecute(r) } @@ -3785,10 +3793,10 @@ identity logged in instead. More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiGetSettingsFlowRequest + - @return FrontendAPIApiGetSettingsFlowRequest */ -func (a *FrontendApiService) GetSettingsFlow(ctx context.Context) FrontendApiApiGetSettingsFlowRequest { - return FrontendApiApiGetSettingsFlowRequest{ +func (a *FrontendAPIService) GetSettingsFlow(ctx context.Context) FrontendAPIApiGetSettingsFlowRequest { + return FrontendAPIApiGetSettingsFlowRequest{ ApiService: a, ctx: ctx, } @@ -3798,7 +3806,7 @@ func (a *FrontendApiService) GetSettingsFlow(ctx context.Context) FrontendApiApi * Execute executes the request * @return SettingsFlow */ -func (a *FrontendApiService) GetSettingsFlowExecute(r FrontendApiApiGetSettingsFlowRequest) (*SettingsFlow, *http.Response, error) { +func (a *FrontendAPIService) GetSettingsFlowExecute(r FrontendAPIApiGetSettingsFlowRequest) (*SettingsFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -3808,7 +3816,7 @@ func (a *FrontendApiService) GetSettingsFlowExecute(r FrontendApiApiGetSettingsF localVarReturnValue *SettingsFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.GetSettingsFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.GetSettingsFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3856,7 +3864,7 @@ func (a *FrontendApiService) GetSettingsFlowExecute(r FrontendApiApiGetSettingsF return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3930,23 +3938,23 @@ func (a *FrontendApiService) GetSettingsFlowExecute(r FrontendApiApiGetSettingsF return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiGetVerificationFlowRequest struct { +type FrontendAPIApiGetVerificationFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI id *string cookie *string } -func (r FrontendApiApiGetVerificationFlowRequest) Id(id string) FrontendApiApiGetVerificationFlowRequest { +func (r FrontendAPIApiGetVerificationFlowRequest) Id(id string) FrontendAPIApiGetVerificationFlowRequest { r.id = &id return r } -func (r FrontendApiApiGetVerificationFlowRequest) Cookie(cookie string) FrontendApiApiGetVerificationFlowRequest { +func (r FrontendAPIApiGetVerificationFlowRequest) Cookie(cookie string) FrontendAPIApiGetVerificationFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiGetVerificationFlowRequest) Execute() (*VerificationFlow, *http.Response, error) { +func (r FrontendAPIApiGetVerificationFlowRequest) Execute() (*VerificationFlow, *http.Response, error) { return r.ApiService.GetVerificationFlowExecute(r) } @@ -3971,10 +3979,10 @@ res.render('verification', flow) More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiGetVerificationFlowRequest + - @return FrontendAPIApiGetVerificationFlowRequest */ -func (a *FrontendApiService) GetVerificationFlow(ctx context.Context) FrontendApiApiGetVerificationFlowRequest { - return FrontendApiApiGetVerificationFlowRequest{ +func (a *FrontendAPIService) GetVerificationFlow(ctx context.Context) FrontendAPIApiGetVerificationFlowRequest { + return FrontendAPIApiGetVerificationFlowRequest{ ApiService: a, ctx: ctx, } @@ -3984,7 +3992,7 @@ func (a *FrontendApiService) GetVerificationFlow(ctx context.Context) FrontendAp * Execute executes the request * @return VerificationFlow */ -func (a *FrontendApiService) GetVerificationFlowExecute(r FrontendApiApiGetVerificationFlowRequest) (*VerificationFlow, *http.Response, error) { +func (a *FrontendAPIService) GetVerificationFlowExecute(r FrontendAPIApiGetVerificationFlowRequest) (*VerificationFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -3994,7 +4002,7 @@ func (a *FrontendApiService) GetVerificationFlowExecute(r FrontendApiApiGetVerif localVarReturnValue *VerificationFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.GetVerificationFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.GetVerificationFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -4039,7 +4047,7 @@ func (a *FrontendApiService) GetVerificationFlowExecute(r FrontendApiApiGetVerif return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4093,12 +4101,12 @@ func (a *FrontendApiService) GetVerificationFlowExecute(r FrontendApiApiGetVerif return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiGetWebAuthnJavaScriptRequest struct { +type FrontendAPIApiGetWebAuthnJavaScriptRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI } -func (r FrontendApiApiGetWebAuthnJavaScriptRequest) Execute() (string, *http.Response, error) { +func (r FrontendAPIApiGetWebAuthnJavaScriptRequest) Execute() (string, *http.Response, error) { return r.ApiService.GetWebAuthnJavaScriptExecute(r) } @@ -4114,10 +4122,10 @@ If you are building a JavaScript Browser App (e.g. in ReactJS or AngularJS) you More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiGetWebAuthnJavaScriptRequest + - @return FrontendAPIApiGetWebAuthnJavaScriptRequest */ -func (a *FrontendApiService) GetWebAuthnJavaScript(ctx context.Context) FrontendApiApiGetWebAuthnJavaScriptRequest { - return FrontendApiApiGetWebAuthnJavaScriptRequest{ +func (a *FrontendAPIService) GetWebAuthnJavaScript(ctx context.Context) FrontendAPIApiGetWebAuthnJavaScriptRequest { + return FrontendAPIApiGetWebAuthnJavaScriptRequest{ ApiService: a, ctx: ctx, } @@ -4127,7 +4135,7 @@ func (a *FrontendApiService) GetWebAuthnJavaScript(ctx context.Context) Frontend * Execute executes the request * @return string */ -func (a *FrontendApiService) GetWebAuthnJavaScriptExecute(r FrontendApiApiGetWebAuthnJavaScriptRequest) (string, *http.Response, error) { +func (a *FrontendAPIService) GetWebAuthnJavaScriptExecute(r FrontendAPIApiGetWebAuthnJavaScriptRequest) (string, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -4137,7 +4145,7 @@ func (a *FrontendApiService) GetWebAuthnJavaScriptExecute(r FrontendApiApiGetWeb localVarReturnValue string ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.GetWebAuthnJavaScript") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.GetWebAuthnJavaScript") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -4175,7 +4183,7 @@ func (a *FrontendApiService) GetWebAuthnJavaScriptExecute(r FrontendApiApiGetWeb return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4202,9 +4210,9 @@ func (a *FrontendApiService) GetWebAuthnJavaScriptExecute(r FrontendApiApiGetWeb return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiListMySessionsRequest struct { +type FrontendAPIApiListMySessionsRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI perPage *int64 page *int64 pageSize *int64 @@ -4213,32 +4221,32 @@ type FrontendApiApiListMySessionsRequest struct { cookie *string } -func (r FrontendApiApiListMySessionsRequest) PerPage(perPage int64) FrontendApiApiListMySessionsRequest { +func (r FrontendAPIApiListMySessionsRequest) PerPage(perPage int64) FrontendAPIApiListMySessionsRequest { r.perPage = &perPage return r } -func (r FrontendApiApiListMySessionsRequest) Page(page int64) FrontendApiApiListMySessionsRequest { +func (r FrontendAPIApiListMySessionsRequest) Page(page int64) FrontendAPIApiListMySessionsRequest { r.page = &page return r } -func (r FrontendApiApiListMySessionsRequest) PageSize(pageSize int64) FrontendApiApiListMySessionsRequest { +func (r FrontendAPIApiListMySessionsRequest) PageSize(pageSize int64) FrontendAPIApiListMySessionsRequest { r.pageSize = &pageSize return r } -func (r FrontendApiApiListMySessionsRequest) PageToken(pageToken string) FrontendApiApiListMySessionsRequest { +func (r FrontendAPIApiListMySessionsRequest) PageToken(pageToken string) FrontendAPIApiListMySessionsRequest { r.pageToken = &pageToken return r } -func (r FrontendApiApiListMySessionsRequest) XSessionToken(xSessionToken string) FrontendApiApiListMySessionsRequest { +func (r FrontendAPIApiListMySessionsRequest) XSessionToken(xSessionToken string) FrontendAPIApiListMySessionsRequest { r.xSessionToken = &xSessionToken return r } -func (r FrontendApiApiListMySessionsRequest) Cookie(cookie string) FrontendApiApiListMySessionsRequest { +func (r FrontendAPIApiListMySessionsRequest) Cookie(cookie string) FrontendAPIApiListMySessionsRequest { r.cookie = &cookie return r } -func (r FrontendApiApiListMySessionsRequest) Execute() ([]Session, *http.Response, error) { +func (r FrontendAPIApiListMySessionsRequest) Execute() ([]Session, *http.Response, error) { return r.ApiService.ListMySessionsExecute(r) } @@ -4248,10 +4256,10 @@ func (r FrontendApiApiListMySessionsRequest) Execute() ([]Session, *http.Respons The current session can be retrieved by calling the `/sessions/whoami` endpoint. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiListMySessionsRequest + - @return FrontendAPIApiListMySessionsRequest */ -func (a *FrontendApiService) ListMySessions(ctx context.Context) FrontendApiApiListMySessionsRequest { - return FrontendApiApiListMySessionsRequest{ +func (a *FrontendAPIService) ListMySessions(ctx context.Context) FrontendAPIApiListMySessionsRequest { + return FrontendAPIApiListMySessionsRequest{ ApiService: a, ctx: ctx, } @@ -4261,7 +4269,7 @@ func (a *FrontendApiService) ListMySessions(ctx context.Context) FrontendApiApiL * Execute executes the request * @return []Session */ -func (a *FrontendApiService) ListMySessionsExecute(r FrontendApiApiListMySessionsRequest) ([]Session, *http.Response, error) { +func (a *FrontendAPIService) ListMySessionsExecute(r FrontendAPIApiListMySessionsRequest) ([]Session, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -4271,7 +4279,7 @@ func (a *FrontendApiService) ListMySessionsExecute(r FrontendApiApiListMySession localVarReturnValue []Session ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.ListMySessions") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.ListMySessions") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -4327,7 +4335,7 @@ func (a *FrontendApiService) ListMySessionsExecute(r FrontendApiApiListMySession return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4381,18 +4389,18 @@ func (a *FrontendApiService) ListMySessionsExecute(r FrontendApiApiListMySession return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiPerformNativeLogoutRequest struct { +type FrontendAPIApiPerformNativeLogoutRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI performNativeLogoutBody *PerformNativeLogoutBody } -func (r FrontendApiApiPerformNativeLogoutRequest) PerformNativeLogoutBody(performNativeLogoutBody PerformNativeLogoutBody) FrontendApiApiPerformNativeLogoutRequest { +func (r FrontendAPIApiPerformNativeLogoutRequest) PerformNativeLogoutBody(performNativeLogoutBody PerformNativeLogoutBody) FrontendAPIApiPerformNativeLogoutRequest { r.performNativeLogoutBody = &performNativeLogoutBody return r } -func (r FrontendApiApiPerformNativeLogoutRequest) Execute() (*http.Response, error) { +func (r FrontendAPIApiPerformNativeLogoutRequest) Execute() (*http.Response, error) { return r.ApiService.PerformNativeLogoutExecute(r) } @@ -4408,10 +4416,10 @@ If the Ory Session Token is malformed or does not exist a 403 Forbidden response This endpoint does not remove any HTTP Cookies - use the Browser-Based Self-Service Logout Flow instead. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiPerformNativeLogoutRequest + - @return FrontendAPIApiPerformNativeLogoutRequest */ -func (a *FrontendApiService) PerformNativeLogout(ctx context.Context) FrontendApiApiPerformNativeLogoutRequest { - return FrontendApiApiPerformNativeLogoutRequest{ +func (a *FrontendAPIService) PerformNativeLogout(ctx context.Context) FrontendAPIApiPerformNativeLogoutRequest { + return FrontendAPIApiPerformNativeLogoutRequest{ ApiService: a, ctx: ctx, } @@ -4420,7 +4428,7 @@ func (a *FrontendApiService) PerformNativeLogout(ctx context.Context) FrontendAp /* * Execute executes the request */ -func (a *FrontendApiService) PerformNativeLogoutExecute(r FrontendApiApiPerformNativeLogoutRequest) (*http.Response, error) { +func (a *FrontendAPIService) PerformNativeLogoutExecute(r FrontendAPIApiPerformNativeLogoutRequest) (*http.Response, error) { var ( localVarHTTPMethod = http.MethodDelete localVarPostBody interface{} @@ -4429,7 +4437,7 @@ func (a *FrontendApiService) PerformNativeLogoutExecute(r FrontendApiApiPerformN localVarFileBytes []byte ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.PerformNativeLogout") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.PerformNativeLogout") if err != nil { return nil, &GenericOpenAPIError{error: err.Error()} } @@ -4472,7 +4480,7 @@ func (a *FrontendApiService) PerformNativeLogoutExecute(r FrontendApiApiPerformN return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4507,28 +4515,28 @@ func (a *FrontendApiService) PerformNativeLogoutExecute(r FrontendApiApiPerformN return localVarHTTPResponse, nil } -type FrontendApiApiToSessionRequest struct { +type FrontendAPIApiToSessionRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI xSessionToken *string cookie *string tokenizeAs *string } -func (r FrontendApiApiToSessionRequest) XSessionToken(xSessionToken string) FrontendApiApiToSessionRequest { +func (r FrontendAPIApiToSessionRequest) XSessionToken(xSessionToken string) FrontendAPIApiToSessionRequest { r.xSessionToken = &xSessionToken return r } -func (r FrontendApiApiToSessionRequest) Cookie(cookie string) FrontendApiApiToSessionRequest { +func (r FrontendAPIApiToSessionRequest) Cookie(cookie string) FrontendAPIApiToSessionRequest { r.cookie = &cookie return r } -func (r FrontendApiApiToSessionRequest) TokenizeAs(tokenizeAs string) FrontendApiApiToSessionRequest { +func (r FrontendAPIApiToSessionRequest) TokenizeAs(tokenizeAs string) FrontendAPIApiToSessionRequest { r.tokenizeAs = &tokenizeAs return r } -func (r FrontendApiApiToSessionRequest) Execute() (*Session, *http.Response, error) { +func (r FrontendAPIApiToSessionRequest) Execute() (*Session, *http.Response, error) { return r.ApiService.ToSessionExecute(r) } @@ -4596,10 +4604,10 @@ As explained above, this request may fail due to several reasons. The `error.id` `session_inactive`: No active session was found in the request (e.g. no Ory Session Cookie / Ory Session Token). `session_aal2_required`: An active session was found but it does not fulfil the Authenticator Assurance Level, implying that the session must (e.g.) authenticate the second factor. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiToSessionRequest + - @return FrontendAPIApiToSessionRequest */ -func (a *FrontendApiService) ToSession(ctx context.Context) FrontendApiApiToSessionRequest { - return FrontendApiApiToSessionRequest{ +func (a *FrontendAPIService) ToSession(ctx context.Context) FrontendAPIApiToSessionRequest { + return FrontendAPIApiToSessionRequest{ ApiService: a, ctx: ctx, } @@ -4609,7 +4617,7 @@ func (a *FrontendApiService) ToSession(ctx context.Context) FrontendApiApiToSess * Execute executes the request * @return Session */ -func (a *FrontendApiService) ToSessionExecute(r FrontendApiApiToSessionRequest) (*Session, *http.Response, error) { +func (a *FrontendAPIService) ToSessionExecute(r FrontendAPIApiToSessionRequest) (*Session, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -4619,7 +4627,7 @@ func (a *FrontendApiService) ToSessionExecute(r FrontendApiApiToSessionRequest) localVarReturnValue *Session ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.ToSession") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.ToSession") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -4666,7 +4674,7 @@ func (a *FrontendApiService) ToSessionExecute(r FrontendApiApiToSessionRequest) return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4720,33 +4728,33 @@ func (a *FrontendApiService) ToSessionExecute(r FrontendApiApiToSessionRequest) return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiUpdateLoginFlowRequest struct { +type FrontendAPIApiUpdateLoginFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI flow *string updateLoginFlowBody *UpdateLoginFlowBody xSessionToken *string cookie *string } -func (r FrontendApiApiUpdateLoginFlowRequest) Flow(flow string) FrontendApiApiUpdateLoginFlowRequest { +func (r FrontendAPIApiUpdateLoginFlowRequest) Flow(flow string) FrontendAPIApiUpdateLoginFlowRequest { r.flow = &flow return r } -func (r FrontendApiApiUpdateLoginFlowRequest) UpdateLoginFlowBody(updateLoginFlowBody UpdateLoginFlowBody) FrontendApiApiUpdateLoginFlowRequest { +func (r FrontendAPIApiUpdateLoginFlowRequest) UpdateLoginFlowBody(updateLoginFlowBody UpdateLoginFlowBody) FrontendAPIApiUpdateLoginFlowRequest { r.updateLoginFlowBody = &updateLoginFlowBody return r } -func (r FrontendApiApiUpdateLoginFlowRequest) XSessionToken(xSessionToken string) FrontendApiApiUpdateLoginFlowRequest { +func (r FrontendAPIApiUpdateLoginFlowRequest) XSessionToken(xSessionToken string) FrontendAPIApiUpdateLoginFlowRequest { r.xSessionToken = &xSessionToken return r } -func (r FrontendApiApiUpdateLoginFlowRequest) Cookie(cookie string) FrontendApiApiUpdateLoginFlowRequest { +func (r FrontendAPIApiUpdateLoginFlowRequest) Cookie(cookie string) FrontendAPIApiUpdateLoginFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiUpdateLoginFlowRequest) Execute() (*SuccessfulNativeLogin, *http.Response, error) { +func (r FrontendAPIApiUpdateLoginFlowRequest) Execute() (*SuccessfulNativeLogin, *http.Response, error) { return r.ApiService.UpdateLoginFlowExecute(r) } @@ -4781,10 +4789,10 @@ Most likely used in Social Sign In flows. More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiUpdateLoginFlowRequest + - @return FrontendAPIApiUpdateLoginFlowRequest */ -func (a *FrontendApiService) UpdateLoginFlow(ctx context.Context) FrontendApiApiUpdateLoginFlowRequest { - return FrontendApiApiUpdateLoginFlowRequest{ +func (a *FrontendAPIService) UpdateLoginFlow(ctx context.Context) FrontendAPIApiUpdateLoginFlowRequest { + return FrontendAPIApiUpdateLoginFlowRequest{ ApiService: a, ctx: ctx, } @@ -4794,7 +4802,7 @@ func (a *FrontendApiService) UpdateLoginFlow(ctx context.Context) FrontendApiApi * Execute executes the request * @return SuccessfulNativeLogin */ -func (a *FrontendApiService) UpdateLoginFlowExecute(r FrontendApiApiUpdateLoginFlowRequest) (*SuccessfulNativeLogin, *http.Response, error) { +func (a *FrontendAPIService) UpdateLoginFlowExecute(r FrontendAPIApiUpdateLoginFlowRequest) (*SuccessfulNativeLogin, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPost localVarPostBody interface{} @@ -4804,7 +4812,7 @@ func (a *FrontendApiService) UpdateLoginFlowExecute(r FrontendApiApiUpdateLoginF localVarReturnValue *SuccessfulNativeLogin ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.UpdateLoginFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.UpdateLoginFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -4857,7 +4865,7 @@ func (a *FrontendApiService) UpdateLoginFlowExecute(r FrontendApiApiUpdateLoginF return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -4921,28 +4929,28 @@ func (a *FrontendApiService) UpdateLoginFlowExecute(r FrontendApiApiUpdateLoginF return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiUpdateLogoutFlowRequest struct { +type FrontendAPIApiUpdateLogoutFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI token *string returnTo *string cookie *string } -func (r FrontendApiApiUpdateLogoutFlowRequest) Token(token string) FrontendApiApiUpdateLogoutFlowRequest { +func (r FrontendAPIApiUpdateLogoutFlowRequest) Token(token string) FrontendAPIApiUpdateLogoutFlowRequest { r.token = &token return r } -func (r FrontendApiApiUpdateLogoutFlowRequest) ReturnTo(returnTo string) FrontendApiApiUpdateLogoutFlowRequest { +func (r FrontendAPIApiUpdateLogoutFlowRequest) ReturnTo(returnTo string) FrontendAPIApiUpdateLogoutFlowRequest { r.returnTo = &returnTo return r } -func (r FrontendApiApiUpdateLogoutFlowRequest) Cookie(cookie string) FrontendApiApiUpdateLogoutFlowRequest { +func (r FrontendAPIApiUpdateLogoutFlowRequest) Cookie(cookie string) FrontendAPIApiUpdateLogoutFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiUpdateLogoutFlowRequest) Execute() (*http.Response, error) { +func (r FrontendAPIApiUpdateLogoutFlowRequest) Execute() (*http.Response, error) { return r.ApiService.UpdateLogoutFlowExecute(r) } @@ -4962,10 +4970,10 @@ call the `/self-service/logout/api` URL directly with the Ory Session Token. More information can be found at [Ory Kratos User Logout Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-logout). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiUpdateLogoutFlowRequest + - @return FrontendAPIApiUpdateLogoutFlowRequest */ -func (a *FrontendApiService) UpdateLogoutFlow(ctx context.Context) FrontendApiApiUpdateLogoutFlowRequest { - return FrontendApiApiUpdateLogoutFlowRequest{ +func (a *FrontendAPIService) UpdateLogoutFlow(ctx context.Context) FrontendAPIApiUpdateLogoutFlowRequest { + return FrontendAPIApiUpdateLogoutFlowRequest{ ApiService: a, ctx: ctx, } @@ -4974,7 +4982,7 @@ func (a *FrontendApiService) UpdateLogoutFlow(ctx context.Context) FrontendApiAp /* * Execute executes the request */ -func (a *FrontendApiService) UpdateLogoutFlowExecute(r FrontendApiApiUpdateLogoutFlowRequest) (*http.Response, error) { +func (a *FrontendAPIService) UpdateLogoutFlowExecute(r FrontendAPIApiUpdateLogoutFlowRequest) (*http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -4983,7 +4991,7 @@ func (a *FrontendApiService) UpdateLogoutFlowExecute(r FrontendApiApiUpdateLogou localVarFileBytes []byte ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.UpdateLogoutFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.UpdateLogoutFlow") if err != nil { return nil, &GenericOpenAPIError{error: err.Error()} } @@ -5030,7 +5038,7 @@ func (a *FrontendApiService) UpdateLogoutFlowExecute(r FrontendApiApiUpdateLogou return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -5055,33 +5063,33 @@ func (a *FrontendApiService) UpdateLogoutFlowExecute(r FrontendApiApiUpdateLogou return localVarHTTPResponse, nil } -type FrontendApiApiUpdateRecoveryFlowRequest struct { +type FrontendAPIApiUpdateRecoveryFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI flow *string updateRecoveryFlowBody *UpdateRecoveryFlowBody token *string cookie *string } -func (r FrontendApiApiUpdateRecoveryFlowRequest) Flow(flow string) FrontendApiApiUpdateRecoveryFlowRequest { +func (r FrontendAPIApiUpdateRecoveryFlowRequest) Flow(flow string) FrontendAPIApiUpdateRecoveryFlowRequest { r.flow = &flow return r } -func (r FrontendApiApiUpdateRecoveryFlowRequest) UpdateRecoveryFlowBody(updateRecoveryFlowBody UpdateRecoveryFlowBody) FrontendApiApiUpdateRecoveryFlowRequest { +func (r FrontendAPIApiUpdateRecoveryFlowRequest) UpdateRecoveryFlowBody(updateRecoveryFlowBody UpdateRecoveryFlowBody) FrontendAPIApiUpdateRecoveryFlowRequest { r.updateRecoveryFlowBody = &updateRecoveryFlowBody return r } -func (r FrontendApiApiUpdateRecoveryFlowRequest) Token(token string) FrontendApiApiUpdateRecoveryFlowRequest { +func (r FrontendAPIApiUpdateRecoveryFlowRequest) Token(token string) FrontendAPIApiUpdateRecoveryFlowRequest { r.token = &token return r } -func (r FrontendApiApiUpdateRecoveryFlowRequest) Cookie(cookie string) FrontendApiApiUpdateRecoveryFlowRequest { +func (r FrontendAPIApiUpdateRecoveryFlowRequest) Cookie(cookie string) FrontendAPIApiUpdateRecoveryFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiUpdateRecoveryFlowRequest) Execute() (*RecoveryFlow, *http.Response, error) { +func (r FrontendAPIApiUpdateRecoveryFlowRequest) Execute() (*RecoveryFlow, *http.Response, error) { return r.ApiService.UpdateRecoveryFlowExecute(r) } @@ -5105,10 +5113,10 @@ a new Recovery Flow ID which contains an error message that the recovery link wa More information can be found at [Ory Kratos Account Recovery Documentation](../self-service/flows/account-recovery). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiUpdateRecoveryFlowRequest + - @return FrontendAPIApiUpdateRecoveryFlowRequest */ -func (a *FrontendApiService) UpdateRecoveryFlow(ctx context.Context) FrontendApiApiUpdateRecoveryFlowRequest { - return FrontendApiApiUpdateRecoveryFlowRequest{ +func (a *FrontendAPIService) UpdateRecoveryFlow(ctx context.Context) FrontendAPIApiUpdateRecoveryFlowRequest { + return FrontendAPIApiUpdateRecoveryFlowRequest{ ApiService: a, ctx: ctx, } @@ -5118,7 +5126,7 @@ func (a *FrontendApiService) UpdateRecoveryFlow(ctx context.Context) FrontendApi * Execute executes the request * @return RecoveryFlow */ -func (a *FrontendApiService) UpdateRecoveryFlowExecute(r FrontendApiApiUpdateRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) { +func (a *FrontendAPIService) UpdateRecoveryFlowExecute(r FrontendAPIApiUpdateRecoveryFlowRequest) (*RecoveryFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPost localVarPostBody interface{} @@ -5128,7 +5136,7 @@ func (a *FrontendApiService) UpdateRecoveryFlowExecute(r FrontendApiApiUpdateRec localVarReturnValue *RecoveryFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.UpdateRecoveryFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.UpdateRecoveryFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -5181,7 +5189,7 @@ func (a *FrontendApiService) UpdateRecoveryFlowExecute(r FrontendApiApiUpdateRec return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -5245,28 +5253,28 @@ func (a *FrontendApiService) UpdateRecoveryFlowExecute(r FrontendApiApiUpdateRec return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiUpdateRegistrationFlowRequest struct { +type FrontendAPIApiUpdateRegistrationFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI flow *string updateRegistrationFlowBody *UpdateRegistrationFlowBody cookie *string } -func (r FrontendApiApiUpdateRegistrationFlowRequest) Flow(flow string) FrontendApiApiUpdateRegistrationFlowRequest { +func (r FrontendAPIApiUpdateRegistrationFlowRequest) Flow(flow string) FrontendAPIApiUpdateRegistrationFlowRequest { r.flow = &flow return r } -func (r FrontendApiApiUpdateRegistrationFlowRequest) UpdateRegistrationFlowBody(updateRegistrationFlowBody UpdateRegistrationFlowBody) FrontendApiApiUpdateRegistrationFlowRequest { +func (r FrontendAPIApiUpdateRegistrationFlowRequest) UpdateRegistrationFlowBody(updateRegistrationFlowBody UpdateRegistrationFlowBody) FrontendAPIApiUpdateRegistrationFlowRequest { r.updateRegistrationFlowBody = &updateRegistrationFlowBody return r } -func (r FrontendApiApiUpdateRegistrationFlowRequest) Cookie(cookie string) FrontendApiApiUpdateRegistrationFlowRequest { +func (r FrontendAPIApiUpdateRegistrationFlowRequest) Cookie(cookie string) FrontendAPIApiUpdateRegistrationFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiUpdateRegistrationFlowRequest) Execute() (*SuccessfulNativeRegistration, *http.Response, error) { +func (r FrontendAPIApiUpdateRegistrationFlowRequest) Execute() (*SuccessfulNativeRegistration, *http.Response, error) { return r.ApiService.UpdateRegistrationFlowExecute(r) } @@ -5302,10 +5310,10 @@ Most likely used in Social Sign In flows. More information can be found at [Ory Kratos User Login](https://www.ory.sh/docs/kratos/self-service/flows/user-login) and [User Registration Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-registration). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiUpdateRegistrationFlowRequest + - @return FrontendAPIApiUpdateRegistrationFlowRequest */ -func (a *FrontendApiService) UpdateRegistrationFlow(ctx context.Context) FrontendApiApiUpdateRegistrationFlowRequest { - return FrontendApiApiUpdateRegistrationFlowRequest{ +func (a *FrontendAPIService) UpdateRegistrationFlow(ctx context.Context) FrontendAPIApiUpdateRegistrationFlowRequest { + return FrontendAPIApiUpdateRegistrationFlowRequest{ ApiService: a, ctx: ctx, } @@ -5315,7 +5323,7 @@ func (a *FrontendApiService) UpdateRegistrationFlow(ctx context.Context) Fronten * Execute executes the request * @return SuccessfulNativeRegistration */ -func (a *FrontendApiService) UpdateRegistrationFlowExecute(r FrontendApiApiUpdateRegistrationFlowRequest) (*SuccessfulNativeRegistration, *http.Response, error) { +func (a *FrontendAPIService) UpdateRegistrationFlowExecute(r FrontendAPIApiUpdateRegistrationFlowRequest) (*SuccessfulNativeRegistration, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPost localVarPostBody interface{} @@ -5325,7 +5333,7 @@ func (a *FrontendApiService) UpdateRegistrationFlowExecute(r FrontendApiApiUpdat localVarReturnValue *SuccessfulNativeRegistration ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.UpdateRegistrationFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.UpdateRegistrationFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -5375,7 +5383,7 @@ func (a *FrontendApiService) UpdateRegistrationFlowExecute(r FrontendApiApiUpdat return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -5439,33 +5447,33 @@ func (a *FrontendApiService) UpdateRegistrationFlowExecute(r FrontendApiApiUpdat return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiUpdateSettingsFlowRequest struct { +type FrontendAPIApiUpdateSettingsFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI flow *string updateSettingsFlowBody *UpdateSettingsFlowBody xSessionToken *string cookie *string } -func (r FrontendApiApiUpdateSettingsFlowRequest) Flow(flow string) FrontendApiApiUpdateSettingsFlowRequest { +func (r FrontendAPIApiUpdateSettingsFlowRequest) Flow(flow string) FrontendAPIApiUpdateSettingsFlowRequest { r.flow = &flow return r } -func (r FrontendApiApiUpdateSettingsFlowRequest) UpdateSettingsFlowBody(updateSettingsFlowBody UpdateSettingsFlowBody) FrontendApiApiUpdateSettingsFlowRequest { +func (r FrontendAPIApiUpdateSettingsFlowRequest) UpdateSettingsFlowBody(updateSettingsFlowBody UpdateSettingsFlowBody) FrontendAPIApiUpdateSettingsFlowRequest { r.updateSettingsFlowBody = &updateSettingsFlowBody return r } -func (r FrontendApiApiUpdateSettingsFlowRequest) XSessionToken(xSessionToken string) FrontendApiApiUpdateSettingsFlowRequest { +func (r FrontendAPIApiUpdateSettingsFlowRequest) XSessionToken(xSessionToken string) FrontendAPIApiUpdateSettingsFlowRequest { r.xSessionToken = &xSessionToken return r } -func (r FrontendApiApiUpdateSettingsFlowRequest) Cookie(cookie string) FrontendApiApiUpdateSettingsFlowRequest { +func (r FrontendAPIApiUpdateSettingsFlowRequest) Cookie(cookie string) FrontendAPIApiUpdateSettingsFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiUpdateSettingsFlowRequest) Execute() (*SettingsFlow, *http.Response, error) { +func (r FrontendAPIApiUpdateSettingsFlowRequest) Execute() (*SettingsFlow, *http.Response, error) { return r.ApiService.UpdateSettingsFlowExecute(r) } @@ -5516,10 +5524,10 @@ Most likely used in Social Sign In flows. More information can be found at [Ory Kratos User Settings & Profile Management Documentation](../self-service/flows/user-settings). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiUpdateSettingsFlowRequest + - @return FrontendAPIApiUpdateSettingsFlowRequest */ -func (a *FrontendApiService) UpdateSettingsFlow(ctx context.Context) FrontendApiApiUpdateSettingsFlowRequest { - return FrontendApiApiUpdateSettingsFlowRequest{ +func (a *FrontendAPIService) UpdateSettingsFlow(ctx context.Context) FrontendAPIApiUpdateSettingsFlowRequest { + return FrontendAPIApiUpdateSettingsFlowRequest{ ApiService: a, ctx: ctx, } @@ -5529,7 +5537,7 @@ func (a *FrontendApiService) UpdateSettingsFlow(ctx context.Context) FrontendApi * Execute executes the request * @return SettingsFlow */ -func (a *FrontendApiService) UpdateSettingsFlowExecute(r FrontendApiApiUpdateSettingsFlowRequest) (*SettingsFlow, *http.Response, error) { +func (a *FrontendAPIService) UpdateSettingsFlowExecute(r FrontendAPIApiUpdateSettingsFlowRequest) (*SettingsFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPost localVarPostBody interface{} @@ -5539,7 +5547,7 @@ func (a *FrontendApiService) UpdateSettingsFlowExecute(r FrontendApiApiUpdateSet localVarReturnValue *SettingsFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.UpdateSettingsFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.UpdateSettingsFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -5592,7 +5600,7 @@ func (a *FrontendApiService) UpdateSettingsFlowExecute(r FrontendApiApiUpdateSet return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -5676,33 +5684,33 @@ func (a *FrontendApiService) UpdateSettingsFlowExecute(r FrontendApiApiUpdateSet return localVarReturnValue, localVarHTTPResponse, nil } -type FrontendApiApiUpdateVerificationFlowRequest struct { +type FrontendAPIApiUpdateVerificationFlowRequest struct { ctx context.Context - ApiService FrontendApi + ApiService FrontendAPI flow *string updateVerificationFlowBody *UpdateVerificationFlowBody token *string cookie *string } -func (r FrontendApiApiUpdateVerificationFlowRequest) Flow(flow string) FrontendApiApiUpdateVerificationFlowRequest { +func (r FrontendAPIApiUpdateVerificationFlowRequest) Flow(flow string) FrontendAPIApiUpdateVerificationFlowRequest { r.flow = &flow return r } -func (r FrontendApiApiUpdateVerificationFlowRequest) UpdateVerificationFlowBody(updateVerificationFlowBody UpdateVerificationFlowBody) FrontendApiApiUpdateVerificationFlowRequest { +func (r FrontendAPIApiUpdateVerificationFlowRequest) UpdateVerificationFlowBody(updateVerificationFlowBody UpdateVerificationFlowBody) FrontendAPIApiUpdateVerificationFlowRequest { r.updateVerificationFlowBody = &updateVerificationFlowBody return r } -func (r FrontendApiApiUpdateVerificationFlowRequest) Token(token string) FrontendApiApiUpdateVerificationFlowRequest { +func (r FrontendAPIApiUpdateVerificationFlowRequest) Token(token string) FrontendAPIApiUpdateVerificationFlowRequest { r.token = &token return r } -func (r FrontendApiApiUpdateVerificationFlowRequest) Cookie(cookie string) FrontendApiApiUpdateVerificationFlowRequest { +func (r FrontendAPIApiUpdateVerificationFlowRequest) Cookie(cookie string) FrontendAPIApiUpdateVerificationFlowRequest { r.cookie = &cookie return r } -func (r FrontendApiApiUpdateVerificationFlowRequest) Execute() (*VerificationFlow, *http.Response, error) { +func (r FrontendAPIApiUpdateVerificationFlowRequest) Execute() (*VerificationFlow, *http.Response, error) { return r.ApiService.UpdateVerificationFlowExecute(r) } @@ -5726,10 +5734,10 @@ a new Verification Flow ID which contains an error message that the verification More information can be found at [Ory Kratos Email and Phone Verification Documentation](https://www.ory.sh/docs/kratos/self-service/flows/verify-email-account-activation). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return FrontendApiApiUpdateVerificationFlowRequest + - @return FrontendAPIApiUpdateVerificationFlowRequest */ -func (a *FrontendApiService) UpdateVerificationFlow(ctx context.Context) FrontendApiApiUpdateVerificationFlowRequest { - return FrontendApiApiUpdateVerificationFlowRequest{ +func (a *FrontendAPIService) UpdateVerificationFlow(ctx context.Context) FrontendAPIApiUpdateVerificationFlowRequest { + return FrontendAPIApiUpdateVerificationFlowRequest{ ApiService: a, ctx: ctx, } @@ -5739,7 +5747,7 @@ func (a *FrontendApiService) UpdateVerificationFlow(ctx context.Context) Fronten * Execute executes the request * @return VerificationFlow */ -func (a *FrontendApiService) UpdateVerificationFlowExecute(r FrontendApiApiUpdateVerificationFlowRequest) (*VerificationFlow, *http.Response, error) { +func (a *FrontendAPIService) UpdateVerificationFlowExecute(r FrontendAPIApiUpdateVerificationFlowRequest) (*VerificationFlow, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPost localVarPostBody interface{} @@ -5749,7 +5757,7 @@ func (a *FrontendApiService) UpdateVerificationFlowExecute(r FrontendApiApiUpdat localVarReturnValue *VerificationFlow ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendApiService.UpdateVerificationFlow") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "FrontendAPIService.UpdateVerificationFlow") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -5802,7 +5810,7 @@ func (a *FrontendApiService) UpdateVerificationFlowExecute(r FrontendApiApiUpdat return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { diff --git a/internal/httpclient/api_identity.go b/internal/httpclient/api_identity.go index e19c70b2df40..60ac8fd7fd9f 100644 --- a/internal/httpclient/api_identity.go +++ b/internal/httpclient/api_identity.go @@ -26,7 +26,7 @@ var ( _ context.Context ) -type IdentityApi interface { +type IdentityAPI interface { /* * AdminCurrentSessionExtend Calling this endpoint refreshes a current user session. If `session.refresh_min_time_left` is set it will only refresh the session after this time has passed. @@ -34,15 +34,15 @@ type IdentityApi interface { Session refresh * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiAdminCurrentSessionExtendRequest + * @return IdentityAPIApiAdminCurrentSessionExtendRequest */ - AdminCurrentSessionExtend(ctx context.Context) IdentityApiApiAdminCurrentSessionExtendRequest + AdminCurrentSessionExtend(ctx context.Context) IdentityAPIApiAdminCurrentSessionExtendRequest /* * AdminCurrentSessionExtendExecute executes the request * @return Session */ - AdminCurrentSessionExtendExecute(r IdentityApiApiAdminCurrentSessionExtendRequest) (*Session, *http.Response, error) + AdminCurrentSessionExtendExecute(r IdentityAPIApiAdminCurrentSessionExtendRequest) (*Session, *http.Response, error) /* * AdminIdentitySession Calling this endpoint issues a session for a given identity. @@ -51,15 +51,15 @@ type IdentityApi interface { Issuing session or session token for a given identity without authenticating * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @return IdentityApiApiAdminIdentitySessionRequest + * @return IdentityAPIApiAdminIdentitySessionRequest */ - AdminIdentitySession(ctx context.Context, id string) IdentityApiApiAdminIdentitySessionRequest + AdminIdentitySession(ctx context.Context, id string) IdentityAPIApiAdminIdentitySessionRequest /* * AdminIdentitySessionExecute executes the request * @return SuccessfulAdminIdentitySession */ - AdminIdentitySessionExecute(r IdentityApiApiAdminIdentitySessionRequest) (*SuccessfulAdminIdentitySession, *http.Response, error) + AdminIdentitySessionExecute(r IdentityAPIApiAdminIdentitySessionRequest) (*SuccessfulAdminIdentitySession, *http.Response, error) /* * AdminUpdateCredentials Update Identity Credentials @@ -68,14 +68,14 @@ type IdentityApi interface { Learn how identities work in [Ory Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @return IdentityApiApiAdminUpdateCredentialsRequest + * @return IdentityAPIApiAdminUpdateCredentialsRequest */ - AdminUpdateCredentials(ctx context.Context, id string) IdentityApiApiAdminUpdateCredentialsRequest + AdminUpdateCredentials(ctx context.Context, id string) IdentityAPIApiAdminUpdateCredentialsRequest /* * AdminUpdateCredentialsExecute executes the request */ - AdminUpdateCredentialsExecute(r IdentityApiApiAdminUpdateCredentialsRequest) (*http.Response, error) + AdminUpdateCredentialsExecute(r IdentityAPIApiAdminUpdateCredentialsRequest) (*http.Response, error) /* * AdminUpdateIdentityBody Validates provided traits and state @@ -84,33 +84,33 @@ type IdentityApi interface { Learn how identities work in [Ory Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model). * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiAdminUpdateIdentityBodyRequest + * @return IdentityAPIApiAdminUpdateIdentityBodyRequest */ - AdminUpdateIdentityBody(ctx context.Context) IdentityApiApiAdminUpdateIdentityBodyRequest + AdminUpdateIdentityBody(ctx context.Context) IdentityAPIApiAdminUpdateIdentityBodyRequest /* * AdminUpdateIdentityBodyExecute executes the request * @return Identity */ - AdminUpdateIdentityBodyExecute(r IdentityApiApiAdminUpdateIdentityBodyRequest) (*Identity, *http.Response, error) + AdminUpdateIdentityBodyExecute(r IdentityAPIApiAdminUpdateIdentityBodyRequest) (*Identity, *http.Response, error) /* - * BatchPatchIdentities Create and deletes multiple identities - * Creates or delete multiple + * BatchPatchIdentities Create multiple identities + * Creates multiple [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model). This endpoint can also be used to [import credentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities) for instance passwords, social sign in configurations or multifactor methods. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiBatchPatchIdentitiesRequest + * @return IdentityAPIApiBatchPatchIdentitiesRequest */ - BatchPatchIdentities(ctx context.Context) IdentityApiApiBatchPatchIdentitiesRequest + BatchPatchIdentities(ctx context.Context) IdentityAPIApiBatchPatchIdentitiesRequest /* * BatchPatchIdentitiesExecute executes the request * @return BatchPatchIdentitiesResponse */ - BatchPatchIdentitiesExecute(r IdentityApiApiBatchPatchIdentitiesRequest) (*BatchPatchIdentitiesResponse, *http.Response, error) + BatchPatchIdentitiesExecute(r IdentityAPIApiBatchPatchIdentitiesRequest) (*BatchPatchIdentitiesResponse, *http.Response, error) /* * CreateIdentity Create an Identity @@ -118,45 +118,45 @@ type IdentityApi interface { [import credentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities) for instance passwords, social sign in configurations or multifactor methods. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiCreateIdentityRequest + * @return IdentityAPIApiCreateIdentityRequest */ - CreateIdentity(ctx context.Context) IdentityApiApiCreateIdentityRequest + CreateIdentity(ctx context.Context) IdentityAPIApiCreateIdentityRequest /* * CreateIdentityExecute executes the request * @return Identity */ - CreateIdentityExecute(r IdentityApiApiCreateIdentityRequest) (*Identity, *http.Response, error) + CreateIdentityExecute(r IdentityAPIApiCreateIdentityRequest) (*Identity, *http.Response, error) /* * CreateRecoveryCodeForIdentity Create a Recovery Code * This endpoint creates a recovery code which should be given to the user in order for them to recover (or activate) their account. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiCreateRecoveryCodeForIdentityRequest + * @return IdentityAPIApiCreateRecoveryCodeForIdentityRequest */ - CreateRecoveryCodeForIdentity(ctx context.Context) IdentityApiApiCreateRecoveryCodeForIdentityRequest + CreateRecoveryCodeForIdentity(ctx context.Context) IdentityAPIApiCreateRecoveryCodeForIdentityRequest /* * CreateRecoveryCodeForIdentityExecute executes the request * @return RecoveryCodeForIdentity */ - CreateRecoveryCodeForIdentityExecute(r IdentityApiApiCreateRecoveryCodeForIdentityRequest) (*RecoveryCodeForIdentity, *http.Response, error) + CreateRecoveryCodeForIdentityExecute(r IdentityAPIApiCreateRecoveryCodeForIdentityRequest) (*RecoveryCodeForIdentity, *http.Response, error) /* * CreateRecoveryLinkForIdentity Create a Recovery Link * This endpoint creates a recovery link which should be given to the user in order for them to recover (or activate) their account. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiCreateRecoveryLinkForIdentityRequest + * @return IdentityAPIApiCreateRecoveryLinkForIdentityRequest */ - CreateRecoveryLinkForIdentity(ctx context.Context) IdentityApiApiCreateRecoveryLinkForIdentityRequest + CreateRecoveryLinkForIdentity(ctx context.Context) IdentityAPIApiCreateRecoveryLinkForIdentityRequest /* * CreateRecoveryLinkForIdentityExecute executes the request * @return RecoveryLinkForIdentity */ - CreateRecoveryLinkForIdentityExecute(r IdentityApiApiCreateRecoveryLinkForIdentityRequest) (*RecoveryLinkForIdentity, *http.Response, error) + CreateRecoveryLinkForIdentityExecute(r IdentityAPIApiCreateRecoveryLinkForIdentityRequest) (*RecoveryLinkForIdentity, *http.Response, error) /* * DeleteIdentity Delete an Identity @@ -165,76 +165,83 @@ type IdentityApi interface { assumed that is has been deleted already. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @return IdentityApiApiDeleteIdentityRequest + * @return IdentityAPIApiDeleteIdentityRequest */ - DeleteIdentity(ctx context.Context, id string) IdentityApiApiDeleteIdentityRequest + DeleteIdentity(ctx context.Context, id string) IdentityAPIApiDeleteIdentityRequest /* * DeleteIdentityExecute executes the request */ - DeleteIdentityExecute(r IdentityApiApiDeleteIdentityRequest) (*http.Response, error) + DeleteIdentityExecute(r IdentityAPIApiDeleteIdentityRequest) (*http.Response, error) /* * DeleteIdentityCredentials Delete a credential for a specific identity - * Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type - You can only delete second factor (aal2) credentials. + * Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type. + You cannot delete password or code auth credentials through this API. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode - * @return IdentityApiApiDeleteIdentityCredentialsRequest + * @param type_ Type is the type of credentials to delete. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + * @return IdentityAPIApiDeleteIdentityCredentialsRequest */ - DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest + DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityAPIApiDeleteIdentityCredentialsRequest /* * DeleteIdentityCredentialsExecute executes the request */ - DeleteIdentityCredentialsExecute(r IdentityApiApiDeleteIdentityCredentialsRequest) (*http.Response, error) + DeleteIdentityCredentialsExecute(r IdentityAPIApiDeleteIdentityCredentialsRequest) (*http.Response, error) /* * DeleteIdentitySessions Delete & Invalidate an Identity's Sessions * Calling this endpoint irrecoverably and permanently deletes and invalidates all sessions that belong to the given Identity. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @return IdentityApiApiDeleteIdentitySessionsRequest + * @return IdentityAPIApiDeleteIdentitySessionsRequest */ - DeleteIdentitySessions(ctx context.Context, id string) IdentityApiApiDeleteIdentitySessionsRequest + DeleteIdentitySessions(ctx context.Context, id string) IdentityAPIApiDeleteIdentitySessionsRequest /* * DeleteIdentitySessionsExecute executes the request */ - DeleteIdentitySessionsExecute(r IdentityApiApiDeleteIdentitySessionsRequest) (*http.Response, error) + DeleteIdentitySessionsExecute(r IdentityAPIApiDeleteIdentitySessionsRequest) (*http.Response, error) /* * DisableSession Deactivate a Session * Calling this endpoint deactivates the specified session. Session data is not deleted. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the session's ID. - * @return IdentityApiApiDisableSessionRequest + * @return IdentityAPIApiDisableSessionRequest */ - DisableSession(ctx context.Context, id string) IdentityApiApiDisableSessionRequest + DisableSession(ctx context.Context, id string) IdentityAPIApiDisableSessionRequest /* * DisableSessionExecute executes the request */ - DisableSessionExecute(r IdentityApiApiDisableSessionRequest) (*http.Response, error) + DisableSessionExecute(r IdentityAPIApiDisableSessionRequest) (*http.Response, error) /* * ExtendSession Extend a Session * Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it will only extend the session after the specified time has passed. + This endpoint returns per default a 204 No Content response on success. Older Ory Network projects may + return a 200 OK response with the session in the body. Returning the session as part of the response + will be deprecated in the future and should not be relied upon. + + This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those + scenarios. This endpoint also returns 404 errors if the session does not exist. + Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the session's ID. - * @return IdentityApiApiExtendSessionRequest + * @return IdentityAPIApiExtendSessionRequest */ - ExtendSession(ctx context.Context, id string) IdentityApiApiExtendSessionRequest + ExtendSession(ctx context.Context, id string) IdentityAPIApiExtendSessionRequest /* * ExtendSessionExecute executes the request * @return Session */ - ExtendSessionExecute(r IdentityApiApiExtendSessionRequest) (*Session, *http.Response, error) + ExtendSessionExecute(r IdentityAPIApiExtendSessionRequest) (*Session, *http.Response, error) /* * GetIdentity Get an Identity @@ -242,30 +249,30 @@ type IdentityApi interface { include credentials (e.g. social sign in connections) in the response by using the `include_credential` query parameter. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID must be set to the ID of identity you want to get - * @return IdentityApiApiGetIdentityRequest + * @return IdentityAPIApiGetIdentityRequest */ - GetIdentity(ctx context.Context, id string) IdentityApiApiGetIdentityRequest + GetIdentity(ctx context.Context, id string) IdentityAPIApiGetIdentityRequest /* * GetIdentityExecute executes the request * @return Identity */ - GetIdentityExecute(r IdentityApiApiGetIdentityRequest) (*Identity, *http.Response, error) + GetIdentityExecute(r IdentityAPIApiGetIdentityRequest) (*Identity, *http.Response, error) /* * GetIdentitySchema Get Identity JSON Schema * Return a specific identity schema. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID must be set to the ID of schema you want to get - * @return IdentityApiApiGetIdentitySchemaRequest + * @return IdentityAPIApiGetIdentitySchemaRequest */ - GetIdentitySchema(ctx context.Context, id string) IdentityApiApiGetIdentitySchemaRequest + GetIdentitySchema(ctx context.Context, id string) IdentityAPIApiGetIdentitySchemaRequest /* * GetIdentitySchemaExecute executes the request * @return map[string]interface{} */ - GetIdentitySchemaExecute(r IdentityApiApiGetIdentitySchemaRequest) (map[string]interface{}, *http.Response, error) + GetIdentitySchemaExecute(r IdentityAPIApiGetIdentitySchemaRequest) (map[string]interface{}, *http.Response, error) /* * GetSession Get Session @@ -274,72 +281,72 @@ type IdentityApi interface { Getting a session object with all specified expandables that exist in an administrative context. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the session's ID. - * @return IdentityApiApiGetSessionRequest + * @return IdentityAPIApiGetSessionRequest */ - GetSession(ctx context.Context, id string) IdentityApiApiGetSessionRequest + GetSession(ctx context.Context, id string) IdentityAPIApiGetSessionRequest /* * GetSessionExecute executes the request * @return Session */ - GetSessionExecute(r IdentityApiApiGetSessionRequest) (*Session, *http.Response, error) + GetSessionExecute(r IdentityAPIApiGetSessionRequest) (*Session, *http.Response, error) /* * ListIdentities List Identities * Lists all [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model) in the system. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiListIdentitiesRequest + * @return IdentityAPIApiListIdentitiesRequest */ - ListIdentities(ctx context.Context) IdentityApiApiListIdentitiesRequest + ListIdentities(ctx context.Context) IdentityAPIApiListIdentitiesRequest /* * ListIdentitiesExecute executes the request * @return []Identity */ - ListIdentitiesExecute(r IdentityApiApiListIdentitiesRequest) ([]Identity, *http.Response, error) + ListIdentitiesExecute(r IdentityAPIApiListIdentitiesRequest) ([]Identity, *http.Response, error) /* * ListIdentitySchemas Get all Identity Schemas * Returns a list of all identity schemas currently in use. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiListIdentitySchemasRequest + * @return IdentityAPIApiListIdentitySchemasRequest */ - ListIdentitySchemas(ctx context.Context) IdentityApiApiListIdentitySchemasRequest + ListIdentitySchemas(ctx context.Context) IdentityAPIApiListIdentitySchemasRequest /* * ListIdentitySchemasExecute executes the request * @return []IdentitySchemaContainer */ - ListIdentitySchemasExecute(r IdentityApiApiListIdentitySchemasRequest) ([]IdentitySchemaContainer, *http.Response, error) + ListIdentitySchemasExecute(r IdentityAPIApiListIdentitySchemasRequest) ([]IdentitySchemaContainer, *http.Response, error) /* * ListIdentitySessions List an Identity's Sessions * This endpoint returns all sessions that belong to the given Identity. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @return IdentityApiApiListIdentitySessionsRequest + * @return IdentityAPIApiListIdentitySessionsRequest */ - ListIdentitySessions(ctx context.Context, id string) IdentityApiApiListIdentitySessionsRequest + ListIdentitySessions(ctx context.Context, id string) IdentityAPIApiListIdentitySessionsRequest /* * ListIdentitySessionsExecute executes the request * @return []Session */ - ListIdentitySessionsExecute(r IdentityApiApiListIdentitySessionsRequest) ([]Session, *http.Response, error) + ListIdentitySessionsExecute(r IdentityAPIApiListIdentitySessionsRequest) ([]Session, *http.Response, error) /* * ListSessions List All Sessions * Listing all sessions that exist. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiListSessionsRequest + * @return IdentityAPIApiListSessionsRequest */ - ListSessions(ctx context.Context) IdentityApiApiListSessionsRequest + ListSessions(ctx context.Context) IdentityAPIApiListSessionsRequest /* * ListSessionsExecute executes the request * @return []Session */ - ListSessionsExecute(r IdentityApiApiListSessionsRequest) ([]Session, *http.Response, error) + ListSessionsExecute(r IdentityAPIApiListSessionsRequest) ([]Session, *http.Response, error) /* * PatchIdentity Patch an Identity @@ -347,15 +354,15 @@ type IdentityApi interface { The fields `id`, `stateChangedAt` and `credentials` can not be updated using this method. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID must be set to the ID of identity you want to update - * @return IdentityApiApiPatchIdentityRequest + * @return IdentityAPIApiPatchIdentityRequest */ - PatchIdentity(ctx context.Context, id string) IdentityApiApiPatchIdentityRequest + PatchIdentity(ctx context.Context, id string) IdentityAPIApiPatchIdentityRequest /* * PatchIdentityExecute executes the request * @return Identity */ - PatchIdentityExecute(r IdentityApiApiPatchIdentityRequest) (*Identity, *http.Response, error) + PatchIdentityExecute(r IdentityAPIApiPatchIdentityRequest) (*Identity, *http.Response, error) /* * UpdateIdentity Update an Identity @@ -363,26 +370,26 @@ type IdentityApi interface { payload (except credentials) is expected. It is possible to update the identity's credentials as well. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID must be set to the ID of identity you want to update - * @return IdentityApiApiUpdateIdentityRequest + * @return IdentityAPIApiUpdateIdentityRequest */ - UpdateIdentity(ctx context.Context, id string) IdentityApiApiUpdateIdentityRequest + UpdateIdentity(ctx context.Context, id string) IdentityAPIApiUpdateIdentityRequest /* * UpdateIdentityExecute executes the request * @return Identity */ - UpdateIdentityExecute(r IdentityApiApiUpdateIdentityRequest) (*Identity, *http.Response, error) + UpdateIdentityExecute(r IdentityAPIApiUpdateIdentityRequest) (*Identity, *http.Response, error) } -// IdentityApiService IdentityApi service -type IdentityApiService service +// IdentityAPIService IdentityAPI service +type IdentityAPIService service -type IdentityApiApiAdminCurrentSessionExtendRequest struct { +type IdentityAPIApiAdminCurrentSessionExtendRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI } -func (r IdentityApiApiAdminCurrentSessionExtendRequest) Execute() (*Session, *http.Response, error) { +func (r IdentityAPIApiAdminCurrentSessionExtendRequest) Execute() (*Session, *http.Response, error) { return r.ApiService.AdminCurrentSessionExtendExecute(r) } @@ -392,10 +399,10 @@ func (r IdentityApiApiAdminCurrentSessionExtendRequest) Execute() (*Session, *ht Session refresh - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return IdentityApiApiAdminCurrentSessionExtendRequest + - @return IdentityAPIApiAdminCurrentSessionExtendRequest */ -func (a *IdentityApiService) AdminCurrentSessionExtend(ctx context.Context) IdentityApiApiAdminCurrentSessionExtendRequest { - return IdentityApiApiAdminCurrentSessionExtendRequest{ +func (a *IdentityAPIService) AdminCurrentSessionExtend(ctx context.Context) IdentityAPIApiAdminCurrentSessionExtendRequest { + return IdentityAPIApiAdminCurrentSessionExtendRequest{ ApiService: a, ctx: ctx, } @@ -405,7 +412,7 @@ func (a *IdentityApiService) AdminCurrentSessionExtend(ctx context.Context) Iden * Execute executes the request * @return Session */ -func (a *IdentityApiService) AdminCurrentSessionExtendExecute(r IdentityApiApiAdminCurrentSessionExtendRequest) (*Session, *http.Response, error) { +func (a *IdentityAPIService) AdminCurrentSessionExtendExecute(r IdentityAPIApiAdminCurrentSessionExtendRequest) (*Session, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -415,7 +422,7 @@ func (a *IdentityApiService) AdminCurrentSessionExtendExecute(r IdentityApiApiAd localVarReturnValue *Session ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.AdminCurrentSessionExtend") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.AdminCurrentSessionExtend") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -513,19 +520,19 @@ func (a *IdentityApiService) AdminCurrentSessionExtendExecute(r IdentityApiApiAd return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiAdminIdentitySessionRequest struct { +type IdentityAPIApiAdminIdentitySessionRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string upgrade *bool } -func (r IdentityApiApiAdminIdentitySessionRequest) Upgrade(upgrade bool) IdentityApiApiAdminIdentitySessionRequest { +func (r IdentityAPIApiAdminIdentitySessionRequest) Upgrade(upgrade bool) IdentityAPIApiAdminIdentitySessionRequest { r.upgrade = &upgrade return r } -func (r IdentityApiApiAdminIdentitySessionRequest) Execute() (*SuccessfulAdminIdentitySession, *http.Response, error) { +func (r IdentityAPIApiAdminIdentitySessionRequest) Execute() (*SuccessfulAdminIdentitySession, *http.Response, error) { return r.ApiService.AdminIdentitySessionExecute(r) } @@ -536,10 +543,10 @@ func (r IdentityApiApiAdminIdentitySessionRequest) Execute() (*SuccessfulAdminId Issuing session or session token for a given identity without authenticating - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the identity's ID. - - @return IdentityApiApiAdminIdentitySessionRequest + - @return IdentityAPIApiAdminIdentitySessionRequest */ -func (a *IdentityApiService) AdminIdentitySession(ctx context.Context, id string) IdentityApiApiAdminIdentitySessionRequest { - return IdentityApiApiAdminIdentitySessionRequest{ +func (a *IdentityAPIService) AdminIdentitySession(ctx context.Context, id string) IdentityAPIApiAdminIdentitySessionRequest { + return IdentityAPIApiAdminIdentitySessionRequest{ ApiService: a, ctx: ctx, id: id, @@ -550,7 +557,7 @@ func (a *IdentityApiService) AdminIdentitySession(ctx context.Context, id string * Execute executes the request * @return SuccessfulAdminIdentitySession */ -func (a *IdentityApiService) AdminIdentitySessionExecute(r IdentityApiApiAdminIdentitySessionRequest) (*SuccessfulAdminIdentitySession, *http.Response, error) { +func (a *IdentityAPIService) AdminIdentitySessionExecute(r IdentityAPIApiAdminIdentitySessionRequest) (*SuccessfulAdminIdentitySession, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -560,7 +567,7 @@ func (a *IdentityApiService) AdminIdentitySessionExecute(r IdentityApiApiAdminId localVarReturnValue *SuccessfulAdminIdentitySession ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.AdminIdentitySession") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.AdminIdentitySession") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -662,13 +669,13 @@ func (a *IdentityApiService) AdminIdentitySessionExecute(r IdentityApiApiAdminId return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiAdminUpdateCredentialsRequest struct { +type IdentityAPIApiAdminUpdateCredentialsRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string } -func (r IdentityApiApiAdminUpdateCredentialsRequest) Execute() (*http.Response, error) { +func (r IdentityAPIApiAdminUpdateCredentialsRequest) Execute() (*http.Response, error) { return r.ApiService.AdminUpdateCredentialsExecute(r) } @@ -679,10 +686,10 @@ func (r IdentityApiApiAdminUpdateCredentialsRequest) Execute() (*http.Response, Learn how identities work in [Ory Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the identity's ID. - - @return IdentityApiApiAdminUpdateCredentialsRequest + - @return IdentityAPIApiAdminUpdateCredentialsRequest */ -func (a *IdentityApiService) AdminUpdateCredentials(ctx context.Context, id string) IdentityApiApiAdminUpdateCredentialsRequest { - return IdentityApiApiAdminUpdateCredentialsRequest{ +func (a *IdentityAPIService) AdminUpdateCredentials(ctx context.Context, id string) IdentityAPIApiAdminUpdateCredentialsRequest { + return IdentityAPIApiAdminUpdateCredentialsRequest{ ApiService: a, ctx: ctx, id: id, @@ -692,7 +699,7 @@ func (a *IdentityApiService) AdminUpdateCredentials(ctx context.Context, id stri /* * Execute executes the request */ -func (a *IdentityApiService) AdminUpdateCredentialsExecute(r IdentityApiApiAdminUpdateCredentialsRequest) (*http.Response, error) { +func (a *IdentityAPIService) AdminUpdateCredentialsExecute(r IdentityAPIApiAdminUpdateCredentialsRequest) (*http.Response, error) { var ( localVarHTTPMethod = http.MethodPut localVarPostBody interface{} @@ -701,7 +708,7 @@ func (a *IdentityApiService) AdminUpdateCredentialsExecute(r IdentityApiApiAdmin localVarFileBytes []byte ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.AdminUpdateCredentials") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.AdminUpdateCredentials") if err != nil { return nil, &GenericOpenAPIError{error: err.Error()} } @@ -777,12 +784,12 @@ func (a *IdentityApiService) AdminUpdateCredentialsExecute(r IdentityApiApiAdmin return localVarHTTPResponse, nil } -type IdentityApiApiAdminUpdateIdentityBodyRequest struct { +type IdentityAPIApiAdminUpdateIdentityBodyRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI } -func (r IdentityApiApiAdminUpdateIdentityBodyRequest) Execute() (*Identity, *http.Response, error) { +func (r IdentityAPIApiAdminUpdateIdentityBodyRequest) Execute() (*Identity, *http.Response, error) { return r.ApiService.AdminUpdateIdentityBodyExecute(r) } @@ -794,10 +801,10 @@ The full identity payload (except credentials) is expected. This endpoint does n Learn how identities work in [Ory Kratos' User And Identity Model Documentation](https://www.ory.sh/docs/next/kratos/concepts/identity-user-model). - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return IdentityApiApiAdminUpdateIdentityBodyRequest + - @return IdentityAPIApiAdminUpdateIdentityBodyRequest */ -func (a *IdentityApiService) AdminUpdateIdentityBody(ctx context.Context) IdentityApiApiAdminUpdateIdentityBodyRequest { - return IdentityApiApiAdminUpdateIdentityBodyRequest{ +func (a *IdentityAPIService) AdminUpdateIdentityBody(ctx context.Context) IdentityAPIApiAdminUpdateIdentityBodyRequest { + return IdentityAPIApiAdminUpdateIdentityBodyRequest{ ApiService: a, ctx: ctx, } @@ -807,7 +814,7 @@ func (a *IdentityApiService) AdminUpdateIdentityBody(ctx context.Context) Identi * Execute executes the request * @return Identity */ -func (a *IdentityApiService) AdminUpdateIdentityBodyExecute(r IdentityApiApiAdminUpdateIdentityBodyRequest) (*Identity, *http.Response, error) { +func (a *IdentityAPIService) AdminUpdateIdentityBodyExecute(r IdentityAPIApiAdminUpdateIdentityBodyRequest) (*Identity, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPost localVarPostBody interface{} @@ -817,7 +824,7 @@ func (a *IdentityApiService) AdminUpdateIdentityBodyExecute(r IdentityApiApiAdmi localVarReturnValue *Identity ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.AdminUpdateIdentityBody") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.AdminUpdateIdentityBody") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -901,34 +908,34 @@ func (a *IdentityApiService) AdminUpdateIdentityBodyExecute(r IdentityApiApiAdmi return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiBatchPatchIdentitiesRequest struct { +type IdentityAPIApiBatchPatchIdentitiesRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI patchIdentitiesBody *PatchIdentitiesBody } -func (r IdentityApiApiBatchPatchIdentitiesRequest) PatchIdentitiesBody(patchIdentitiesBody PatchIdentitiesBody) IdentityApiApiBatchPatchIdentitiesRequest { +func (r IdentityAPIApiBatchPatchIdentitiesRequest) PatchIdentitiesBody(patchIdentitiesBody PatchIdentitiesBody) IdentityAPIApiBatchPatchIdentitiesRequest { r.patchIdentitiesBody = &patchIdentitiesBody return r } -func (r IdentityApiApiBatchPatchIdentitiesRequest) Execute() (*BatchPatchIdentitiesResponse, *http.Response, error) { +func (r IdentityAPIApiBatchPatchIdentitiesRequest) Execute() (*BatchPatchIdentitiesResponse, *http.Response, error) { return r.ApiService.BatchPatchIdentitiesExecute(r) } /* - - BatchPatchIdentities Create and deletes multiple identities - - Creates or delete multiple + - BatchPatchIdentities Create multiple identities + - Creates multiple [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model). This endpoint can also be used to [import credentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities) for instance passwords, social sign in configurations or multifactor methods. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return IdentityApiApiBatchPatchIdentitiesRequest + - @return IdentityAPIApiBatchPatchIdentitiesRequest */ -func (a *IdentityApiService) BatchPatchIdentities(ctx context.Context) IdentityApiApiBatchPatchIdentitiesRequest { - return IdentityApiApiBatchPatchIdentitiesRequest{ +func (a *IdentityAPIService) BatchPatchIdentities(ctx context.Context) IdentityAPIApiBatchPatchIdentitiesRequest { + return IdentityAPIApiBatchPatchIdentitiesRequest{ ApiService: a, ctx: ctx, } @@ -938,7 +945,7 @@ func (a *IdentityApiService) BatchPatchIdentities(ctx context.Context) IdentityA * Execute executes the request * @return BatchPatchIdentitiesResponse */ -func (a *IdentityApiService) BatchPatchIdentitiesExecute(r IdentityApiApiBatchPatchIdentitiesRequest) (*BatchPatchIdentitiesResponse, *http.Response, error) { +func (a *IdentityAPIService) BatchPatchIdentitiesExecute(r IdentityAPIApiBatchPatchIdentitiesRequest) (*BatchPatchIdentitiesResponse, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPatch localVarPostBody interface{} @@ -948,7 +955,7 @@ func (a *IdentityApiService) BatchPatchIdentitiesExecute(r IdentityApiApiBatchPa localVarReturnValue *BatchPatchIdentitiesResponse ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.BatchPatchIdentities") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.BatchPatchIdentities") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1002,7 +1009,7 @@ func (a *IdentityApiService) BatchPatchIdentitiesExecute(r IdentityApiApiBatchPa return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1056,18 +1063,18 @@ func (a *IdentityApiService) BatchPatchIdentitiesExecute(r IdentityApiApiBatchPa return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiCreateIdentityRequest struct { +type IdentityAPIApiCreateIdentityRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI createIdentityBody *CreateIdentityBody } -func (r IdentityApiApiCreateIdentityRequest) CreateIdentityBody(createIdentityBody CreateIdentityBody) IdentityApiApiCreateIdentityRequest { +func (r IdentityAPIApiCreateIdentityRequest) CreateIdentityBody(createIdentityBody CreateIdentityBody) IdentityAPIApiCreateIdentityRequest { r.createIdentityBody = &createIdentityBody return r } -func (r IdentityApiApiCreateIdentityRequest) Execute() (*Identity, *http.Response, error) { +func (r IdentityAPIApiCreateIdentityRequest) Execute() (*Identity, *http.Response, error) { return r.ApiService.CreateIdentityExecute(r) } @@ -1078,10 +1085,10 @@ func (r IdentityApiApiCreateIdentityRequest) Execute() (*Identity, *http.Respons [import credentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities) for instance passwords, social sign in configurations or multifactor methods. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return IdentityApiApiCreateIdentityRequest + - @return IdentityAPIApiCreateIdentityRequest */ -func (a *IdentityApiService) CreateIdentity(ctx context.Context) IdentityApiApiCreateIdentityRequest { - return IdentityApiApiCreateIdentityRequest{ +func (a *IdentityAPIService) CreateIdentity(ctx context.Context) IdentityAPIApiCreateIdentityRequest { + return IdentityAPIApiCreateIdentityRequest{ ApiService: a, ctx: ctx, } @@ -1091,7 +1098,7 @@ func (a *IdentityApiService) CreateIdentity(ctx context.Context) IdentityApiApiC * Execute executes the request * @return Identity */ -func (a *IdentityApiService) CreateIdentityExecute(r IdentityApiApiCreateIdentityRequest) (*Identity, *http.Response, error) { +func (a *IdentityAPIService) CreateIdentityExecute(r IdentityAPIApiCreateIdentityRequest) (*Identity, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPost localVarPostBody interface{} @@ -1101,7 +1108,7 @@ func (a *IdentityApiService) CreateIdentityExecute(r IdentityApiApiCreateIdentit localVarReturnValue *Identity ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.CreateIdentity") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.CreateIdentity") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1155,7 +1162,7 @@ func (a *IdentityApiService) CreateIdentityExecute(r IdentityApiApiCreateIdentit return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1209,18 +1216,18 @@ func (a *IdentityApiService) CreateIdentityExecute(r IdentityApiApiCreateIdentit return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiCreateRecoveryCodeForIdentityRequest struct { +type IdentityAPIApiCreateRecoveryCodeForIdentityRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI createRecoveryCodeForIdentityBody *CreateRecoveryCodeForIdentityBody } -func (r IdentityApiApiCreateRecoveryCodeForIdentityRequest) CreateRecoveryCodeForIdentityBody(createRecoveryCodeForIdentityBody CreateRecoveryCodeForIdentityBody) IdentityApiApiCreateRecoveryCodeForIdentityRequest { +func (r IdentityAPIApiCreateRecoveryCodeForIdentityRequest) CreateRecoveryCodeForIdentityBody(createRecoveryCodeForIdentityBody CreateRecoveryCodeForIdentityBody) IdentityAPIApiCreateRecoveryCodeForIdentityRequest { r.createRecoveryCodeForIdentityBody = &createRecoveryCodeForIdentityBody return r } -func (r IdentityApiApiCreateRecoveryCodeForIdentityRequest) Execute() (*RecoveryCodeForIdentity, *http.Response, error) { +func (r IdentityAPIApiCreateRecoveryCodeForIdentityRequest) Execute() (*RecoveryCodeForIdentity, *http.Response, error) { return r.ApiService.CreateRecoveryCodeForIdentityExecute(r) } @@ -1230,10 +1237,10 @@ func (r IdentityApiApiCreateRecoveryCodeForIdentityRequest) Execute() (*Recovery (or activate) their account. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return IdentityApiApiCreateRecoveryCodeForIdentityRequest + - @return IdentityAPIApiCreateRecoveryCodeForIdentityRequest */ -func (a *IdentityApiService) CreateRecoveryCodeForIdentity(ctx context.Context) IdentityApiApiCreateRecoveryCodeForIdentityRequest { - return IdentityApiApiCreateRecoveryCodeForIdentityRequest{ +func (a *IdentityAPIService) CreateRecoveryCodeForIdentity(ctx context.Context) IdentityAPIApiCreateRecoveryCodeForIdentityRequest { + return IdentityAPIApiCreateRecoveryCodeForIdentityRequest{ ApiService: a, ctx: ctx, } @@ -1243,7 +1250,7 @@ func (a *IdentityApiService) CreateRecoveryCodeForIdentity(ctx context.Context) * Execute executes the request * @return RecoveryCodeForIdentity */ -func (a *IdentityApiService) CreateRecoveryCodeForIdentityExecute(r IdentityApiApiCreateRecoveryCodeForIdentityRequest) (*RecoveryCodeForIdentity, *http.Response, error) { +func (a *IdentityAPIService) CreateRecoveryCodeForIdentityExecute(r IdentityAPIApiCreateRecoveryCodeForIdentityRequest) (*RecoveryCodeForIdentity, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPost localVarPostBody interface{} @@ -1253,7 +1260,7 @@ func (a *IdentityApiService) CreateRecoveryCodeForIdentityExecute(r IdentityApiA localVarReturnValue *RecoveryCodeForIdentity ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.CreateRecoveryCodeForIdentity") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.CreateRecoveryCodeForIdentity") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1307,7 +1314,7 @@ func (a *IdentityApiService) CreateRecoveryCodeForIdentityExecute(r IdentityApiA return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1361,23 +1368,23 @@ func (a *IdentityApiService) CreateRecoveryCodeForIdentityExecute(r IdentityApiA return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiCreateRecoveryLinkForIdentityRequest struct { +type IdentityAPIApiCreateRecoveryLinkForIdentityRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI returnTo *string createRecoveryLinkForIdentityBody *CreateRecoveryLinkForIdentityBody } -func (r IdentityApiApiCreateRecoveryLinkForIdentityRequest) ReturnTo(returnTo string) IdentityApiApiCreateRecoveryLinkForIdentityRequest { +func (r IdentityAPIApiCreateRecoveryLinkForIdentityRequest) ReturnTo(returnTo string) IdentityAPIApiCreateRecoveryLinkForIdentityRequest { r.returnTo = &returnTo return r } -func (r IdentityApiApiCreateRecoveryLinkForIdentityRequest) CreateRecoveryLinkForIdentityBody(createRecoveryLinkForIdentityBody CreateRecoveryLinkForIdentityBody) IdentityApiApiCreateRecoveryLinkForIdentityRequest { +func (r IdentityAPIApiCreateRecoveryLinkForIdentityRequest) CreateRecoveryLinkForIdentityBody(createRecoveryLinkForIdentityBody CreateRecoveryLinkForIdentityBody) IdentityAPIApiCreateRecoveryLinkForIdentityRequest { r.createRecoveryLinkForIdentityBody = &createRecoveryLinkForIdentityBody return r } -func (r IdentityApiApiCreateRecoveryLinkForIdentityRequest) Execute() (*RecoveryLinkForIdentity, *http.Response, error) { +func (r IdentityAPIApiCreateRecoveryLinkForIdentityRequest) Execute() (*RecoveryLinkForIdentity, *http.Response, error) { return r.ApiService.CreateRecoveryLinkForIdentityExecute(r) } @@ -1387,10 +1394,10 @@ func (r IdentityApiApiCreateRecoveryLinkForIdentityRequest) Execute() (*Recovery (or activate) their account. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return IdentityApiApiCreateRecoveryLinkForIdentityRequest + - @return IdentityAPIApiCreateRecoveryLinkForIdentityRequest */ -func (a *IdentityApiService) CreateRecoveryLinkForIdentity(ctx context.Context) IdentityApiApiCreateRecoveryLinkForIdentityRequest { - return IdentityApiApiCreateRecoveryLinkForIdentityRequest{ +func (a *IdentityAPIService) CreateRecoveryLinkForIdentity(ctx context.Context) IdentityAPIApiCreateRecoveryLinkForIdentityRequest { + return IdentityAPIApiCreateRecoveryLinkForIdentityRequest{ ApiService: a, ctx: ctx, } @@ -1400,7 +1407,7 @@ func (a *IdentityApiService) CreateRecoveryLinkForIdentity(ctx context.Context) * Execute executes the request * @return RecoveryLinkForIdentity */ -func (a *IdentityApiService) CreateRecoveryLinkForIdentityExecute(r IdentityApiApiCreateRecoveryLinkForIdentityRequest) (*RecoveryLinkForIdentity, *http.Response, error) { +func (a *IdentityAPIService) CreateRecoveryLinkForIdentityExecute(r IdentityAPIApiCreateRecoveryLinkForIdentityRequest) (*RecoveryLinkForIdentity, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPost localVarPostBody interface{} @@ -1410,7 +1417,7 @@ func (a *IdentityApiService) CreateRecoveryLinkForIdentityExecute(r IdentityApiA localVarReturnValue *RecoveryLinkForIdentity ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.CreateRecoveryLinkForIdentity") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.CreateRecoveryLinkForIdentity") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -1467,7 +1474,7 @@ func (a *IdentityApiService) CreateRecoveryLinkForIdentityExecute(r IdentityApiA return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1521,13 +1528,13 @@ func (a *IdentityApiService) CreateRecoveryLinkForIdentityExecute(r IdentityApiA return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiDeleteIdentityRequest struct { +type IdentityAPIApiDeleteIdentityRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string } -func (r IdentityApiApiDeleteIdentityRequest) Execute() (*http.Response, error) { +func (r IdentityAPIApiDeleteIdentityRequest) Execute() (*http.Response, error) { return r.ApiService.DeleteIdentityExecute(r) } @@ -1539,10 +1546,10 @@ This endpoint returns 204 when the identity was deleted or when the identity was assumed that is has been deleted already. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the identity's ID. - - @return IdentityApiApiDeleteIdentityRequest + - @return IdentityAPIApiDeleteIdentityRequest */ -func (a *IdentityApiService) DeleteIdentity(ctx context.Context, id string) IdentityApiApiDeleteIdentityRequest { - return IdentityApiApiDeleteIdentityRequest{ +func (a *IdentityAPIService) DeleteIdentity(ctx context.Context, id string) IdentityAPIApiDeleteIdentityRequest { + return IdentityAPIApiDeleteIdentityRequest{ ApiService: a, ctx: ctx, id: id, @@ -1552,7 +1559,7 @@ func (a *IdentityApiService) DeleteIdentity(ctx context.Context, id string) Iden /* * Execute executes the request */ -func (a *IdentityApiService) DeleteIdentityExecute(r IdentityApiApiDeleteIdentityRequest) (*http.Response, error) { +func (a *IdentityAPIService) DeleteIdentityExecute(r IdentityAPIApiDeleteIdentityRequest) (*http.Response, error) { var ( localVarHTTPMethod = http.MethodDelete localVarPostBody interface{} @@ -1561,7 +1568,7 @@ func (a *IdentityApiService) DeleteIdentityExecute(r IdentityApiApiDeleteIdentit localVarFileBytes []byte ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.DeleteIdentity") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.DeleteIdentity") if err != nil { return nil, &GenericOpenAPIError{error: err.Error()} } @@ -1614,7 +1621,7 @@ func (a *IdentityApiService) DeleteIdentityExecute(r IdentityApiApiDeleteIdentit return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1649,29 +1656,35 @@ func (a *IdentityApiService) DeleteIdentityExecute(r IdentityApiApiDeleteIdentit return localVarHTTPResponse, nil } -type IdentityApiApiDeleteIdentityCredentialsRequest struct { +type IdentityAPIApiDeleteIdentityCredentialsRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string type_ string + identifier *string +} + +func (r IdentityAPIApiDeleteIdentityCredentialsRequest) Identifier(identifier string) IdentityAPIApiDeleteIdentityCredentialsRequest { + r.identifier = &identifier + return r } -func (r IdentityApiApiDeleteIdentityCredentialsRequest) Execute() (*http.Response, error) { +func (r IdentityAPIApiDeleteIdentityCredentialsRequest) Execute() (*http.Response, error) { return r.ApiService.DeleteIdentityCredentialsExecute(r) } /* - DeleteIdentityCredentials Delete a credential for a specific identity - - Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type + - Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type. -You can only delete second factor (aal2) credentials. +You cannot delete password or code auth credentials through this API. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the identity's ID. - - @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode - - @return IdentityApiApiDeleteIdentityCredentialsRequest + - @param type_ Type is the type of credentials to delete. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + - @return IdentityAPIApiDeleteIdentityCredentialsRequest */ -func (a *IdentityApiService) DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest { - return IdentityApiApiDeleteIdentityCredentialsRequest{ +func (a *IdentityAPIService) DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityAPIApiDeleteIdentityCredentialsRequest { + return IdentityAPIApiDeleteIdentityCredentialsRequest{ ApiService: a, ctx: ctx, id: id, @@ -1682,7 +1695,7 @@ func (a *IdentityApiService) DeleteIdentityCredentials(ctx context.Context, id s /* * Execute executes the request */ -func (a *IdentityApiService) DeleteIdentityCredentialsExecute(r IdentityApiApiDeleteIdentityCredentialsRequest) (*http.Response, error) { +func (a *IdentityAPIService) DeleteIdentityCredentialsExecute(r IdentityAPIApiDeleteIdentityCredentialsRequest) (*http.Response, error) { var ( localVarHTTPMethod = http.MethodDelete localVarPostBody interface{} @@ -1691,7 +1704,7 @@ func (a *IdentityApiService) DeleteIdentityCredentialsExecute(r IdentityApiApiDe localVarFileBytes []byte ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.DeleteIdentityCredentials") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.DeleteIdentityCredentials") if err != nil { return nil, &GenericOpenAPIError{error: err.Error()} } @@ -1704,6 +1717,9 @@ func (a *IdentityApiService) DeleteIdentityCredentialsExecute(r IdentityApiApiDe localVarQueryParams := url.Values{} localVarFormParams := url.Values{} + if r.identifier != nil { + localVarQueryParams.Add("identifier", parameterToString(*r.identifier, "")) + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} @@ -1745,7 +1761,7 @@ func (a *IdentityApiService) DeleteIdentityCredentialsExecute(r IdentityApiApiDe return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1780,13 +1796,13 @@ func (a *IdentityApiService) DeleteIdentityCredentialsExecute(r IdentityApiApiDe return localVarHTTPResponse, nil } -type IdentityApiApiDeleteIdentitySessionsRequest struct { +type IdentityAPIApiDeleteIdentitySessionsRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string } -func (r IdentityApiApiDeleteIdentitySessionsRequest) Execute() (*http.Response, error) { +func (r IdentityAPIApiDeleteIdentitySessionsRequest) Execute() (*http.Response, error) { return r.ApiService.DeleteIdentitySessionsExecute(r) } @@ -1795,10 +1811,10 @@ func (r IdentityApiApiDeleteIdentitySessionsRequest) Execute() (*http.Response, * Calling this endpoint irrecoverably and permanently deletes and invalidates all sessions that belong to the given Identity. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @return IdentityApiApiDeleteIdentitySessionsRequest + * @return IdentityAPIApiDeleteIdentitySessionsRequest */ -func (a *IdentityApiService) DeleteIdentitySessions(ctx context.Context, id string) IdentityApiApiDeleteIdentitySessionsRequest { - return IdentityApiApiDeleteIdentitySessionsRequest{ +func (a *IdentityAPIService) DeleteIdentitySessions(ctx context.Context, id string) IdentityAPIApiDeleteIdentitySessionsRequest { + return IdentityAPIApiDeleteIdentitySessionsRequest{ ApiService: a, ctx: ctx, id: id, @@ -1808,7 +1824,7 @@ func (a *IdentityApiService) DeleteIdentitySessions(ctx context.Context, id stri /* * Execute executes the request */ -func (a *IdentityApiService) DeleteIdentitySessionsExecute(r IdentityApiApiDeleteIdentitySessionsRequest) (*http.Response, error) { +func (a *IdentityAPIService) DeleteIdentitySessionsExecute(r IdentityAPIApiDeleteIdentitySessionsRequest) (*http.Response, error) { var ( localVarHTTPMethod = http.MethodDelete localVarPostBody interface{} @@ -1817,7 +1833,7 @@ func (a *IdentityApiService) DeleteIdentitySessionsExecute(r IdentityApiApiDelet localVarFileBytes []byte ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.DeleteIdentitySessions") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.DeleteIdentitySessions") if err != nil { return nil, &GenericOpenAPIError{error: err.Error()} } @@ -1870,7 +1886,7 @@ func (a *IdentityApiService) DeleteIdentitySessionsExecute(r IdentityApiApiDelet return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -1925,13 +1941,13 @@ func (a *IdentityApiService) DeleteIdentitySessionsExecute(r IdentityApiApiDelet return localVarHTTPResponse, nil } -type IdentityApiApiDisableSessionRequest struct { +type IdentityAPIApiDisableSessionRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string } -func (r IdentityApiApiDisableSessionRequest) Execute() (*http.Response, error) { +func (r IdentityAPIApiDisableSessionRequest) Execute() (*http.Response, error) { return r.ApiService.DisableSessionExecute(r) } @@ -1940,10 +1956,10 @@ func (r IdentityApiApiDisableSessionRequest) Execute() (*http.Response, error) { * Calling this endpoint deactivates the specified session. Session data is not deleted. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the session's ID. - * @return IdentityApiApiDisableSessionRequest + * @return IdentityAPIApiDisableSessionRequest */ -func (a *IdentityApiService) DisableSession(ctx context.Context, id string) IdentityApiApiDisableSessionRequest { - return IdentityApiApiDisableSessionRequest{ +func (a *IdentityAPIService) DisableSession(ctx context.Context, id string) IdentityAPIApiDisableSessionRequest { + return IdentityAPIApiDisableSessionRequest{ ApiService: a, ctx: ctx, id: id, @@ -1953,7 +1969,7 @@ func (a *IdentityApiService) DisableSession(ctx context.Context, id string) Iden /* * Execute executes the request */ -func (a *IdentityApiService) DisableSessionExecute(r IdentityApiApiDisableSessionRequest) (*http.Response, error) { +func (a *IdentityAPIService) DisableSessionExecute(r IdentityAPIApiDisableSessionRequest) (*http.Response, error) { var ( localVarHTTPMethod = http.MethodDelete localVarPostBody interface{} @@ -1962,7 +1978,7 @@ func (a *IdentityApiService) DisableSessionExecute(r IdentityApiApiDisableSessio localVarFileBytes []byte ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.DisableSession") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.DisableSession") if err != nil { return nil, &GenericOpenAPIError{error: err.Error()} } @@ -2015,7 +2031,7 @@ func (a *IdentityApiService) DisableSessionExecute(r IdentityApiApiDisableSessio return localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2060,13 +2076,13 @@ func (a *IdentityApiService) DisableSessionExecute(r IdentityApiApiDisableSessio return localVarHTTPResponse, nil } -type IdentityApiApiExtendSessionRequest struct { +type IdentityAPIApiExtendSessionRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string } -func (r IdentityApiApiExtendSessionRequest) Execute() (*Session, *http.Response, error) { +func (r IdentityAPIApiExtendSessionRequest) Execute() (*Session, *http.Response, error) { return r.ApiService.ExtendSessionExecute(r) } @@ -2076,13 +2092,20 @@ func (r IdentityApiApiExtendSessionRequest) Execute() (*Session, *http.Response, will only extend the session after the specified time has passed. +This endpoint returns per default a 204 No Content response on success. Older Ory Network projects may +return a 200 OK response with the session in the body. Returning the session as part of the response +will be deprecated in the future and should not be relied upon. + +This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those +scenarios. This endpoint also returns 404 errors if the session does not exist. + Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the session's ID. - - @return IdentityApiApiExtendSessionRequest + - @return IdentityAPIApiExtendSessionRequest */ -func (a *IdentityApiService) ExtendSession(ctx context.Context, id string) IdentityApiApiExtendSessionRequest { - return IdentityApiApiExtendSessionRequest{ +func (a *IdentityAPIService) ExtendSession(ctx context.Context, id string) IdentityAPIApiExtendSessionRequest { + return IdentityAPIApiExtendSessionRequest{ ApiService: a, ctx: ctx, id: id, @@ -2093,7 +2116,7 @@ func (a *IdentityApiService) ExtendSession(ctx context.Context, id string) Ident * Execute executes the request * @return Session */ -func (a *IdentityApiService) ExtendSessionExecute(r IdentityApiApiExtendSessionRequest) (*Session, *http.Response, error) { +func (a *IdentityAPIService) ExtendSessionExecute(r IdentityAPIApiExtendSessionRequest) (*Session, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPatch localVarPostBody interface{} @@ -2103,7 +2126,7 @@ func (a *IdentityApiService) ExtendSessionExecute(r IdentityApiApiExtendSessionR localVarReturnValue *Session ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.ExtendSession") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.ExtendSession") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2156,7 +2179,7 @@ func (a *IdentityApiService) ExtendSessionExecute(r IdentityApiApiExtendSessionR return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2210,19 +2233,19 @@ func (a *IdentityApiService) ExtendSessionExecute(r IdentityApiApiExtendSessionR return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiGetIdentityRequest struct { +type IdentityAPIApiGetIdentityRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string includeCredential *[]string } -func (r IdentityApiApiGetIdentityRequest) IncludeCredential(includeCredential []string) IdentityApiApiGetIdentityRequest { +func (r IdentityAPIApiGetIdentityRequest) IncludeCredential(includeCredential []string) IdentityAPIApiGetIdentityRequest { r.includeCredential = &includeCredential return r } -func (r IdentityApiApiGetIdentityRequest) Execute() (*Identity, *http.Response, error) { +func (r IdentityAPIApiGetIdentityRequest) Execute() (*Identity, *http.Response, error) { return r.ApiService.GetIdentityExecute(r) } @@ -2233,10 +2256,10 @@ func (r IdentityApiApiGetIdentityRequest) Execute() (*Identity, *http.Response, include credentials (e.g. social sign in connections) in the response by using the `include_credential` query parameter. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID must be set to the ID of identity you want to get - - @return IdentityApiApiGetIdentityRequest + - @return IdentityAPIApiGetIdentityRequest */ -func (a *IdentityApiService) GetIdentity(ctx context.Context, id string) IdentityApiApiGetIdentityRequest { - return IdentityApiApiGetIdentityRequest{ +func (a *IdentityAPIService) GetIdentity(ctx context.Context, id string) IdentityAPIApiGetIdentityRequest { + return IdentityAPIApiGetIdentityRequest{ ApiService: a, ctx: ctx, id: id, @@ -2247,7 +2270,7 @@ func (a *IdentityApiService) GetIdentity(ctx context.Context, id string) Identit * Execute executes the request * @return Identity */ -func (a *IdentityApiService) GetIdentityExecute(r IdentityApiApiGetIdentityRequest) (*Identity, *http.Response, error) { +func (a *IdentityAPIService) GetIdentityExecute(r IdentityAPIApiGetIdentityRequest) (*Identity, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2257,7 +2280,7 @@ func (a *IdentityApiService) GetIdentityExecute(r IdentityApiApiGetIdentityReque localVarReturnValue *Identity ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.GetIdentity") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.GetIdentity") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2321,7 +2344,7 @@ func (a *IdentityApiService) GetIdentityExecute(r IdentityApiApiGetIdentityReque return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2365,13 +2388,13 @@ func (a *IdentityApiService) GetIdentityExecute(r IdentityApiApiGetIdentityReque return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiGetIdentitySchemaRequest struct { +type IdentityAPIApiGetIdentitySchemaRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string } -func (r IdentityApiApiGetIdentitySchemaRequest) Execute() (map[string]interface{}, *http.Response, error) { +func (r IdentityAPIApiGetIdentitySchemaRequest) Execute() (map[string]interface{}, *http.Response, error) { return r.ApiService.GetIdentitySchemaExecute(r) } @@ -2380,10 +2403,10 @@ func (r IdentityApiApiGetIdentitySchemaRequest) Execute() (map[string]interface{ * Return a specific identity schema. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID must be set to the ID of schema you want to get - * @return IdentityApiApiGetIdentitySchemaRequest + * @return IdentityAPIApiGetIdentitySchemaRequest */ -func (a *IdentityApiService) GetIdentitySchema(ctx context.Context, id string) IdentityApiApiGetIdentitySchemaRequest { - return IdentityApiApiGetIdentitySchemaRequest{ +func (a *IdentityAPIService) GetIdentitySchema(ctx context.Context, id string) IdentityAPIApiGetIdentitySchemaRequest { + return IdentityAPIApiGetIdentitySchemaRequest{ ApiService: a, ctx: ctx, id: id, @@ -2394,7 +2417,7 @@ func (a *IdentityApiService) GetIdentitySchema(ctx context.Context, id string) I * Execute executes the request * @return map[string]interface{} */ -func (a *IdentityApiService) GetIdentitySchemaExecute(r IdentityApiApiGetIdentitySchemaRequest) (map[string]interface{}, *http.Response, error) { +func (a *IdentityAPIService) GetIdentitySchemaExecute(r IdentityAPIApiGetIdentitySchemaRequest) (map[string]interface{}, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2404,7 +2427,7 @@ func (a *IdentityApiService) GetIdentitySchemaExecute(r IdentityApiApiGetIdentit localVarReturnValue map[string]interface{} ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.GetIdentitySchema") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.GetIdentitySchema") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2443,7 +2466,7 @@ func (a *IdentityApiService) GetIdentitySchemaExecute(r IdentityApiApiGetIdentit return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2487,19 +2510,19 @@ func (a *IdentityApiService) GetIdentitySchemaExecute(r IdentityApiApiGetIdentit return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiGetSessionRequest struct { +type IdentityAPIApiGetSessionRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string expand *[]string } -func (r IdentityApiApiGetSessionRequest) Expand(expand []string) IdentityApiApiGetSessionRequest { +func (r IdentityAPIApiGetSessionRequest) Expand(expand []string) IdentityAPIApiGetSessionRequest { r.expand = &expand return r } -func (r IdentityApiApiGetSessionRequest) Execute() (*Session, *http.Response, error) { +func (r IdentityAPIApiGetSessionRequest) Execute() (*Session, *http.Response, error) { return r.ApiService.GetSessionExecute(r) } @@ -2510,10 +2533,10 @@ func (r IdentityApiApiGetSessionRequest) Execute() (*Session, *http.Response, er Getting a session object with all specified expandables that exist in an administrative context. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID is the session's ID. - - @return IdentityApiApiGetSessionRequest + - @return IdentityAPIApiGetSessionRequest */ -func (a *IdentityApiService) GetSession(ctx context.Context, id string) IdentityApiApiGetSessionRequest { - return IdentityApiApiGetSessionRequest{ +func (a *IdentityAPIService) GetSession(ctx context.Context, id string) IdentityAPIApiGetSessionRequest { + return IdentityAPIApiGetSessionRequest{ ApiService: a, ctx: ctx, id: id, @@ -2524,7 +2547,7 @@ func (a *IdentityApiService) GetSession(ctx context.Context, id string) Identity * Execute executes the request * @return Session */ -func (a *IdentityApiService) GetSessionExecute(r IdentityApiApiGetSessionRequest) (*Session, *http.Response, error) { +func (a *IdentityAPIService) GetSessionExecute(r IdentityAPIApiGetSessionRequest) (*Session, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2534,7 +2557,7 @@ func (a *IdentityApiService) GetSessionExecute(r IdentityApiApiGetSessionRequest localVarReturnValue *Session ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.GetSession") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.GetSession") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2598,7 +2621,7 @@ func (a *IdentityApiService) GetSessionExecute(r IdentityApiApiGetSessionRequest return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2642,9 +2665,9 @@ func (a *IdentityApiService) GetSessionExecute(r IdentityApiApiGetSessionRequest return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiListIdentitiesRequest struct { +type IdentityAPIApiListIdentitiesRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI perPage *int64 page *int64 pageSize *int64 @@ -2653,42 +2676,47 @@ type IdentityApiApiListIdentitiesRequest struct { ids *[]string credentialsIdentifier *string previewCredentialsIdentifierSimilar *string + includeCredential *[]string } -func (r IdentityApiApiListIdentitiesRequest) PerPage(perPage int64) IdentityApiApiListIdentitiesRequest { +func (r IdentityAPIApiListIdentitiesRequest) PerPage(perPage int64) IdentityAPIApiListIdentitiesRequest { r.perPage = &perPage return r } -func (r IdentityApiApiListIdentitiesRequest) Page(page int64) IdentityApiApiListIdentitiesRequest { +func (r IdentityAPIApiListIdentitiesRequest) Page(page int64) IdentityAPIApiListIdentitiesRequest { r.page = &page return r } -func (r IdentityApiApiListIdentitiesRequest) PageSize(pageSize int64) IdentityApiApiListIdentitiesRequest { +func (r IdentityAPIApiListIdentitiesRequest) PageSize(pageSize int64) IdentityAPIApiListIdentitiesRequest { r.pageSize = &pageSize return r } -func (r IdentityApiApiListIdentitiesRequest) PageToken(pageToken string) IdentityApiApiListIdentitiesRequest { +func (r IdentityAPIApiListIdentitiesRequest) PageToken(pageToken string) IdentityAPIApiListIdentitiesRequest { r.pageToken = &pageToken return r } -func (r IdentityApiApiListIdentitiesRequest) Consistency(consistency string) IdentityApiApiListIdentitiesRequest { +func (r IdentityAPIApiListIdentitiesRequest) Consistency(consistency string) IdentityAPIApiListIdentitiesRequest { r.consistency = &consistency return r } -func (r IdentityApiApiListIdentitiesRequest) Ids(ids []string) IdentityApiApiListIdentitiesRequest { +func (r IdentityAPIApiListIdentitiesRequest) Ids(ids []string) IdentityAPIApiListIdentitiesRequest { r.ids = &ids return r } -func (r IdentityApiApiListIdentitiesRequest) CredentialsIdentifier(credentialsIdentifier string) IdentityApiApiListIdentitiesRequest { +func (r IdentityAPIApiListIdentitiesRequest) CredentialsIdentifier(credentialsIdentifier string) IdentityAPIApiListIdentitiesRequest { r.credentialsIdentifier = &credentialsIdentifier return r } -func (r IdentityApiApiListIdentitiesRequest) PreviewCredentialsIdentifierSimilar(previewCredentialsIdentifierSimilar string) IdentityApiApiListIdentitiesRequest { +func (r IdentityAPIApiListIdentitiesRequest) PreviewCredentialsIdentifierSimilar(previewCredentialsIdentifierSimilar string) IdentityAPIApiListIdentitiesRequest { r.previewCredentialsIdentifierSimilar = &previewCredentialsIdentifierSimilar return r } +func (r IdentityAPIApiListIdentitiesRequest) IncludeCredential(includeCredential []string) IdentityAPIApiListIdentitiesRequest { + r.includeCredential = &includeCredential + return r +} -func (r IdentityApiApiListIdentitiesRequest) Execute() ([]Identity, *http.Response, error) { +func (r IdentityAPIApiListIdentitiesRequest) Execute() ([]Identity, *http.Response, error) { return r.ApiService.ListIdentitiesExecute(r) } @@ -2696,10 +2724,10 @@ func (r IdentityApiApiListIdentitiesRequest) Execute() ([]Identity, *http.Respon * ListIdentities List Identities * Lists all [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model) in the system. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiListIdentitiesRequest + * @return IdentityAPIApiListIdentitiesRequest */ -func (a *IdentityApiService) ListIdentities(ctx context.Context) IdentityApiApiListIdentitiesRequest { - return IdentityApiApiListIdentitiesRequest{ +func (a *IdentityAPIService) ListIdentities(ctx context.Context) IdentityAPIApiListIdentitiesRequest { + return IdentityAPIApiListIdentitiesRequest{ ApiService: a, ctx: ctx, } @@ -2709,7 +2737,7 @@ func (a *IdentityApiService) ListIdentities(ctx context.Context) IdentityApiApiL * Execute executes the request * @return []Identity */ -func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitiesRequest) ([]Identity, *http.Response, error) { +func (a *IdentityAPIService) ListIdentitiesExecute(r IdentityAPIApiListIdentitiesRequest) ([]Identity, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2719,7 +2747,7 @@ func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitie localVarReturnValue []Identity ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.ListIdentities") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.ListIdentities") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2762,6 +2790,17 @@ func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitie if r.previewCredentialsIdentifierSimilar != nil { localVarQueryParams.Add("preview_credentials_identifier_similar", parameterToString(*r.previewCredentialsIdentifierSimilar, "")) } + if r.includeCredential != nil { + t := *r.includeCredential + if reflect.TypeOf(t).Kind() == reflect.Slice { + s := reflect.ValueOf(t) + for i := 0; i < s.Len(); i++ { + localVarQueryParams.Add("include_credential", parameterToString(s.Index(i), "multi")) + } + } else { + localVarQueryParams.Add("include_credential", parameterToString(t, "multi")) + } + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} @@ -2803,7 +2842,7 @@ func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitie return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2837,33 +2876,33 @@ func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitie return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiListIdentitySchemasRequest struct { +type IdentityAPIApiListIdentitySchemasRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI perPage *int64 page *int64 pageSize *int64 pageToken *string } -func (r IdentityApiApiListIdentitySchemasRequest) PerPage(perPage int64) IdentityApiApiListIdentitySchemasRequest { +func (r IdentityAPIApiListIdentitySchemasRequest) PerPage(perPage int64) IdentityAPIApiListIdentitySchemasRequest { r.perPage = &perPage return r } -func (r IdentityApiApiListIdentitySchemasRequest) Page(page int64) IdentityApiApiListIdentitySchemasRequest { +func (r IdentityAPIApiListIdentitySchemasRequest) Page(page int64) IdentityAPIApiListIdentitySchemasRequest { r.page = &page return r } -func (r IdentityApiApiListIdentitySchemasRequest) PageSize(pageSize int64) IdentityApiApiListIdentitySchemasRequest { +func (r IdentityAPIApiListIdentitySchemasRequest) PageSize(pageSize int64) IdentityAPIApiListIdentitySchemasRequest { r.pageSize = &pageSize return r } -func (r IdentityApiApiListIdentitySchemasRequest) PageToken(pageToken string) IdentityApiApiListIdentitySchemasRequest { +func (r IdentityAPIApiListIdentitySchemasRequest) PageToken(pageToken string) IdentityAPIApiListIdentitySchemasRequest { r.pageToken = &pageToken return r } -func (r IdentityApiApiListIdentitySchemasRequest) Execute() ([]IdentitySchemaContainer, *http.Response, error) { +func (r IdentityAPIApiListIdentitySchemasRequest) Execute() ([]IdentitySchemaContainer, *http.Response, error) { return r.ApiService.ListIdentitySchemasExecute(r) } @@ -2871,10 +2910,10 @@ func (r IdentityApiApiListIdentitySchemasRequest) Execute() ([]IdentitySchemaCon * ListIdentitySchemas Get all Identity Schemas * Returns a list of all identity schemas currently in use. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiListIdentitySchemasRequest + * @return IdentityAPIApiListIdentitySchemasRequest */ -func (a *IdentityApiService) ListIdentitySchemas(ctx context.Context) IdentityApiApiListIdentitySchemasRequest { - return IdentityApiApiListIdentitySchemasRequest{ +func (a *IdentityAPIService) ListIdentitySchemas(ctx context.Context) IdentityAPIApiListIdentitySchemasRequest { + return IdentityAPIApiListIdentitySchemasRequest{ ApiService: a, ctx: ctx, } @@ -2884,7 +2923,7 @@ func (a *IdentityApiService) ListIdentitySchemas(ctx context.Context) IdentityAp * Execute executes the request * @return []IdentitySchemaContainer */ -func (a *IdentityApiService) ListIdentitySchemasExecute(r IdentityApiApiListIdentitySchemasRequest) ([]IdentitySchemaContainer, *http.Response, error) { +func (a *IdentityAPIService) ListIdentitySchemasExecute(r IdentityAPIApiListIdentitySchemasRequest) ([]IdentitySchemaContainer, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -2894,7 +2933,7 @@ func (a *IdentityApiService) ListIdentitySchemasExecute(r IdentityApiApiListIden localVarReturnValue []IdentitySchemaContainer ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.ListIdentitySchemas") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.ListIdentitySchemas") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -2944,7 +2983,7 @@ func (a *IdentityApiService) ListIdentitySchemasExecute(r IdentityApiApiListIden return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -2978,9 +3017,9 @@ func (a *IdentityApiService) ListIdentitySchemasExecute(r IdentityApiApiListIden return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiListIdentitySessionsRequest struct { +type IdentityAPIApiListIdentitySessionsRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string perPage *int64 page *int64 @@ -2989,28 +3028,28 @@ type IdentityApiApiListIdentitySessionsRequest struct { active *bool } -func (r IdentityApiApiListIdentitySessionsRequest) PerPage(perPage int64) IdentityApiApiListIdentitySessionsRequest { +func (r IdentityAPIApiListIdentitySessionsRequest) PerPage(perPage int64) IdentityAPIApiListIdentitySessionsRequest { r.perPage = &perPage return r } -func (r IdentityApiApiListIdentitySessionsRequest) Page(page int64) IdentityApiApiListIdentitySessionsRequest { +func (r IdentityAPIApiListIdentitySessionsRequest) Page(page int64) IdentityAPIApiListIdentitySessionsRequest { r.page = &page return r } -func (r IdentityApiApiListIdentitySessionsRequest) PageSize(pageSize int64) IdentityApiApiListIdentitySessionsRequest { +func (r IdentityAPIApiListIdentitySessionsRequest) PageSize(pageSize int64) IdentityAPIApiListIdentitySessionsRequest { r.pageSize = &pageSize return r } -func (r IdentityApiApiListIdentitySessionsRequest) PageToken(pageToken string) IdentityApiApiListIdentitySessionsRequest { +func (r IdentityAPIApiListIdentitySessionsRequest) PageToken(pageToken string) IdentityAPIApiListIdentitySessionsRequest { r.pageToken = &pageToken return r } -func (r IdentityApiApiListIdentitySessionsRequest) Active(active bool) IdentityApiApiListIdentitySessionsRequest { +func (r IdentityAPIApiListIdentitySessionsRequest) Active(active bool) IdentityAPIApiListIdentitySessionsRequest { r.active = &active return r } -func (r IdentityApiApiListIdentitySessionsRequest) Execute() ([]Session, *http.Response, error) { +func (r IdentityAPIApiListIdentitySessionsRequest) Execute() ([]Session, *http.Response, error) { return r.ApiService.ListIdentitySessionsExecute(r) } @@ -3019,10 +3058,10 @@ func (r IdentityApiApiListIdentitySessionsRequest) Execute() ([]Session, *http.R * This endpoint returns all sessions that belong to the given Identity. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @param id ID is the identity's ID. - * @return IdentityApiApiListIdentitySessionsRequest + * @return IdentityAPIApiListIdentitySessionsRequest */ -func (a *IdentityApiService) ListIdentitySessions(ctx context.Context, id string) IdentityApiApiListIdentitySessionsRequest { - return IdentityApiApiListIdentitySessionsRequest{ +func (a *IdentityAPIService) ListIdentitySessions(ctx context.Context, id string) IdentityAPIApiListIdentitySessionsRequest { + return IdentityAPIApiListIdentitySessionsRequest{ ApiService: a, ctx: ctx, id: id, @@ -3033,7 +3072,7 @@ func (a *IdentityApiService) ListIdentitySessions(ctx context.Context, id string * Execute executes the request * @return []Session */ -func (a *IdentityApiService) ListIdentitySessionsExecute(r IdentityApiApiListIdentitySessionsRequest) ([]Session, *http.Response, error) { +func (a *IdentityAPIService) ListIdentitySessionsExecute(r IdentityAPIApiListIdentitySessionsRequest) ([]Session, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -3043,7 +3082,7 @@ func (a *IdentityApiService) ListIdentitySessionsExecute(r IdentityApiApiListIde localVarReturnValue []Session ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.ListIdentitySessions") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.ListIdentitySessions") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3111,7 +3150,7 @@ func (a *IdentityApiService) ListIdentitySessionsExecute(r IdentityApiApiListIde return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3165,33 +3204,33 @@ func (a *IdentityApiService) ListIdentitySessionsExecute(r IdentityApiApiListIde return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiListSessionsRequest struct { +type IdentityAPIApiListSessionsRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI pageSize *int64 pageToken *string active *bool expand *[]string } -func (r IdentityApiApiListSessionsRequest) PageSize(pageSize int64) IdentityApiApiListSessionsRequest { +func (r IdentityAPIApiListSessionsRequest) PageSize(pageSize int64) IdentityAPIApiListSessionsRequest { r.pageSize = &pageSize return r } -func (r IdentityApiApiListSessionsRequest) PageToken(pageToken string) IdentityApiApiListSessionsRequest { +func (r IdentityAPIApiListSessionsRequest) PageToken(pageToken string) IdentityAPIApiListSessionsRequest { r.pageToken = &pageToken return r } -func (r IdentityApiApiListSessionsRequest) Active(active bool) IdentityApiApiListSessionsRequest { +func (r IdentityAPIApiListSessionsRequest) Active(active bool) IdentityAPIApiListSessionsRequest { r.active = &active return r } -func (r IdentityApiApiListSessionsRequest) Expand(expand []string) IdentityApiApiListSessionsRequest { +func (r IdentityAPIApiListSessionsRequest) Expand(expand []string) IdentityAPIApiListSessionsRequest { r.expand = &expand return r } -func (r IdentityApiApiListSessionsRequest) Execute() ([]Session, *http.Response, error) { +func (r IdentityAPIApiListSessionsRequest) Execute() ([]Session, *http.Response, error) { return r.ApiService.ListSessionsExecute(r) } @@ -3199,10 +3238,10 @@ func (r IdentityApiApiListSessionsRequest) Execute() ([]Session, *http.Response, * ListSessions List All Sessions * Listing all sessions that exist. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return IdentityApiApiListSessionsRequest + * @return IdentityAPIApiListSessionsRequest */ -func (a *IdentityApiService) ListSessions(ctx context.Context) IdentityApiApiListSessionsRequest { - return IdentityApiApiListSessionsRequest{ +func (a *IdentityAPIService) ListSessions(ctx context.Context) IdentityAPIApiListSessionsRequest { + return IdentityAPIApiListSessionsRequest{ ApiService: a, ctx: ctx, } @@ -3212,7 +3251,7 @@ func (a *IdentityApiService) ListSessions(ctx context.Context) IdentityApiApiLis * Execute executes the request * @return []Session */ -func (a *IdentityApiService) ListSessionsExecute(r IdentityApiApiListSessionsRequest) ([]Session, *http.Response, error) { +func (a *IdentityAPIService) ListSessionsExecute(r IdentityAPIApiListSessionsRequest) ([]Session, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -3222,7 +3261,7 @@ func (a *IdentityApiService) ListSessionsExecute(r IdentityApiApiListSessionsReq localVarReturnValue []Session ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.ListSessions") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.ListSessions") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3294,7 +3333,7 @@ func (a *IdentityApiService) ListSessionsExecute(r IdentityApiApiListSessionsReq return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3338,19 +3377,19 @@ func (a *IdentityApiService) ListSessionsExecute(r IdentityApiApiListSessionsReq return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiPatchIdentityRequest struct { +type IdentityAPIApiPatchIdentityRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string jsonPatch *[]JsonPatch } -func (r IdentityApiApiPatchIdentityRequest) JsonPatch(jsonPatch []JsonPatch) IdentityApiApiPatchIdentityRequest { +func (r IdentityAPIApiPatchIdentityRequest) JsonPatch(jsonPatch []JsonPatch) IdentityAPIApiPatchIdentityRequest { r.jsonPatch = &jsonPatch return r } -func (r IdentityApiApiPatchIdentityRequest) Execute() (*Identity, *http.Response, error) { +func (r IdentityAPIApiPatchIdentityRequest) Execute() (*Identity, *http.Response, error) { return r.ApiService.PatchIdentityExecute(r) } @@ -3361,10 +3400,10 @@ func (r IdentityApiApiPatchIdentityRequest) Execute() (*Identity, *http.Response The fields `id`, `stateChangedAt` and `credentials` can not be updated using this method. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID must be set to the ID of identity you want to update - - @return IdentityApiApiPatchIdentityRequest + - @return IdentityAPIApiPatchIdentityRequest */ -func (a *IdentityApiService) PatchIdentity(ctx context.Context, id string) IdentityApiApiPatchIdentityRequest { - return IdentityApiApiPatchIdentityRequest{ +func (a *IdentityAPIService) PatchIdentity(ctx context.Context, id string) IdentityAPIApiPatchIdentityRequest { + return IdentityAPIApiPatchIdentityRequest{ ApiService: a, ctx: ctx, id: id, @@ -3375,7 +3414,7 @@ func (a *IdentityApiService) PatchIdentity(ctx context.Context, id string) Ident * Execute executes the request * @return Identity */ -func (a *IdentityApiService) PatchIdentityExecute(r IdentityApiApiPatchIdentityRequest) (*Identity, *http.Response, error) { +func (a *IdentityAPIService) PatchIdentityExecute(r IdentityAPIApiPatchIdentityRequest) (*Identity, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPatch localVarPostBody interface{} @@ -3385,7 +3424,7 @@ func (a *IdentityApiService) PatchIdentityExecute(r IdentityApiApiPatchIdentityR localVarReturnValue *Identity ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.PatchIdentity") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.PatchIdentity") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3440,7 +3479,7 @@ func (a *IdentityApiService) PatchIdentityExecute(r IdentityApiApiPatchIdentityR return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -3504,19 +3543,19 @@ func (a *IdentityApiService) PatchIdentityExecute(r IdentityApiApiPatchIdentityR return localVarReturnValue, localVarHTTPResponse, nil } -type IdentityApiApiUpdateIdentityRequest struct { +type IdentityAPIApiUpdateIdentityRequest struct { ctx context.Context - ApiService IdentityApi + ApiService IdentityAPI id string updateIdentityBody *UpdateIdentityBody } -func (r IdentityApiApiUpdateIdentityRequest) UpdateIdentityBody(updateIdentityBody UpdateIdentityBody) IdentityApiApiUpdateIdentityRequest { +func (r IdentityAPIApiUpdateIdentityRequest) UpdateIdentityBody(updateIdentityBody UpdateIdentityBody) IdentityAPIApiUpdateIdentityRequest { r.updateIdentityBody = &updateIdentityBody return r } -func (r IdentityApiApiUpdateIdentityRequest) Execute() (*Identity, *http.Response, error) { +func (r IdentityAPIApiUpdateIdentityRequest) Execute() (*Identity, *http.Response, error) { return r.ApiService.UpdateIdentityExecute(r) } @@ -3527,10 +3566,10 @@ func (r IdentityApiApiUpdateIdentityRequest) Execute() (*Identity, *http.Respons payload (except credentials) is expected. It is possible to update the identity's credentials as well. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id ID must be set to the ID of identity you want to update - - @return IdentityApiApiUpdateIdentityRequest + - @return IdentityAPIApiUpdateIdentityRequest */ -func (a *IdentityApiService) UpdateIdentity(ctx context.Context, id string) IdentityApiApiUpdateIdentityRequest { - return IdentityApiApiUpdateIdentityRequest{ +func (a *IdentityAPIService) UpdateIdentity(ctx context.Context, id string) IdentityAPIApiUpdateIdentityRequest { + return IdentityAPIApiUpdateIdentityRequest{ ApiService: a, ctx: ctx, id: id, @@ -3541,7 +3580,7 @@ func (a *IdentityApiService) UpdateIdentity(ctx context.Context, id string) Iden * Execute executes the request * @return Identity */ -func (a *IdentityApiService) UpdateIdentityExecute(r IdentityApiApiUpdateIdentityRequest) (*Identity, *http.Response, error) { +func (a *IdentityAPIService) UpdateIdentityExecute(r IdentityAPIApiUpdateIdentityRequest) (*Identity, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPut localVarPostBody interface{} @@ -3551,7 +3590,7 @@ func (a *IdentityApiService) UpdateIdentityExecute(r IdentityApiApiUpdateIdentit localVarReturnValue *Identity ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityApiService.UpdateIdentity") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "IdentityAPIService.UpdateIdentity") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -3606,7 +3645,7 @@ func (a *IdentityApiService) UpdateIdentityExecute(r IdentityApiApiUpdateIdentit return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { diff --git a/internal/httpclient/api_metadata.go b/internal/httpclient/api_metadata.go index 0ea297189e2f..4bef0d5cb6ca 100644 --- a/internal/httpclient/api_metadata.go +++ b/internal/httpclient/api_metadata.go @@ -24,7 +24,7 @@ var ( _ context.Context ) -type MetadataApi interface { +type MetadataAPI interface { /* * GetVersion Return Running Software Version. @@ -36,15 +36,15 @@ type MetadataApi interface { Be aware that if you are running multiple nodes of this service, the version will never refer to the cluster state, only to a single instance. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return MetadataApiApiGetVersionRequest + * @return MetadataAPIApiGetVersionRequest */ - GetVersion(ctx context.Context) MetadataApiApiGetVersionRequest + GetVersion(ctx context.Context) MetadataAPIApiGetVersionRequest /* * GetVersionExecute executes the request * @return GetVersion200Response */ - GetVersionExecute(r MetadataApiApiGetVersionRequest) (*GetVersion200Response, *http.Response, error) + GetVersionExecute(r MetadataAPIApiGetVersionRequest) (*GetVersion200Response, *http.Response, error) /* * IsAlive Check HTTP Server Status @@ -57,15 +57,15 @@ type MetadataApi interface { Be aware that if you are running multiple nodes of this service, the health status will never refer to the cluster state, only to a single instance. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return MetadataApiApiIsAliveRequest + * @return MetadataAPIApiIsAliveRequest */ - IsAlive(ctx context.Context) MetadataApiApiIsAliveRequest + IsAlive(ctx context.Context) MetadataAPIApiIsAliveRequest /* * IsAliveExecute executes the request * @return IsAlive200Response */ - IsAliveExecute(r MetadataApiApiIsAliveRequest) (*IsAlive200Response, *http.Response, error) + IsAliveExecute(r MetadataAPIApiIsAliveRequest) (*IsAlive200Response, *http.Response, error) /* * IsReady Check HTTP Server and Database Status @@ -78,26 +78,26 @@ type MetadataApi interface { Be aware that if you are running multiple nodes of Ory Kratos, the health status will never refer to the cluster state, only to a single instance. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - * @return MetadataApiApiIsReadyRequest + * @return MetadataAPIApiIsReadyRequest */ - IsReady(ctx context.Context) MetadataApiApiIsReadyRequest + IsReady(ctx context.Context) MetadataAPIApiIsReadyRequest /* * IsReadyExecute executes the request * @return IsAlive200Response */ - IsReadyExecute(r MetadataApiApiIsReadyRequest) (*IsAlive200Response, *http.Response, error) + IsReadyExecute(r MetadataAPIApiIsReadyRequest) (*IsAlive200Response, *http.Response, error) } -// MetadataApiService MetadataApi service -type MetadataApiService service +// MetadataAPIService MetadataAPI service +type MetadataAPIService service -type MetadataApiApiGetVersionRequest struct { +type MetadataAPIApiGetVersionRequest struct { ctx context.Context - ApiService MetadataApi + ApiService MetadataAPI } -func (r MetadataApiApiGetVersionRequest) Execute() (*GetVersion200Response, *http.Response, error) { +func (r MetadataAPIApiGetVersionRequest) Execute() (*GetVersion200Response, *http.Response, error) { return r.ApiService.GetVersionExecute(r) } @@ -111,10 +111,10 @@ If the service supports TLS Edge Termination, this endpoint does not require the Be aware that if you are running multiple nodes of this service, the version will never refer to the cluster state, only to a single instance. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return MetadataApiApiGetVersionRequest + - @return MetadataAPIApiGetVersionRequest */ -func (a *MetadataApiService) GetVersion(ctx context.Context) MetadataApiApiGetVersionRequest { - return MetadataApiApiGetVersionRequest{ +func (a *MetadataAPIService) GetVersion(ctx context.Context) MetadataAPIApiGetVersionRequest { + return MetadataAPIApiGetVersionRequest{ ApiService: a, ctx: ctx, } @@ -124,7 +124,7 @@ func (a *MetadataApiService) GetVersion(ctx context.Context) MetadataApiApiGetVe * Execute executes the request * @return GetVersion200Response */ -func (a *MetadataApiService) GetVersionExecute(r MetadataApiApiGetVersionRequest) (*GetVersion200Response, *http.Response, error) { +func (a *MetadataAPIService) GetVersionExecute(r MetadataAPIApiGetVersionRequest) (*GetVersion200Response, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -134,7 +134,7 @@ func (a *MetadataApiService) GetVersionExecute(r MetadataApiApiGetVersionRequest localVarReturnValue *GetVersion200Response ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataApiService.GetVersion") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataAPIService.GetVersion") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -172,7 +172,7 @@ func (a *MetadataApiService) GetVersionExecute(r MetadataApiApiGetVersionRequest return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -199,12 +199,12 @@ func (a *MetadataApiService) GetVersionExecute(r MetadataApiApiGetVersionRequest return localVarReturnValue, localVarHTTPResponse, nil } -type MetadataApiApiIsAliveRequest struct { +type MetadataAPIApiIsAliveRequest struct { ctx context.Context - ApiService MetadataApi + ApiService MetadataAPI } -func (r MetadataApiApiIsAliveRequest) Execute() (*IsAlive200Response, *http.Response, error) { +func (r MetadataAPIApiIsAliveRequest) Execute() (*IsAlive200Response, *http.Response, error) { return r.ApiService.IsAliveExecute(r) } @@ -220,10 +220,10 @@ If the service supports TLS Edge Termination, this endpoint does not require the Be aware that if you are running multiple nodes of this service, the health status will never refer to the cluster state, only to a single instance. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return MetadataApiApiIsAliveRequest + - @return MetadataAPIApiIsAliveRequest */ -func (a *MetadataApiService) IsAlive(ctx context.Context) MetadataApiApiIsAliveRequest { - return MetadataApiApiIsAliveRequest{ +func (a *MetadataAPIService) IsAlive(ctx context.Context) MetadataAPIApiIsAliveRequest { + return MetadataAPIApiIsAliveRequest{ ApiService: a, ctx: ctx, } @@ -233,7 +233,7 @@ func (a *MetadataApiService) IsAlive(ctx context.Context) MetadataApiApiIsAliveR * Execute executes the request * @return IsAlive200Response */ -func (a *MetadataApiService) IsAliveExecute(r MetadataApiApiIsAliveRequest) (*IsAlive200Response, *http.Response, error) { +func (a *MetadataAPIService) IsAliveExecute(r MetadataAPIApiIsAliveRequest) (*IsAlive200Response, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -243,7 +243,7 @@ func (a *MetadataApiService) IsAliveExecute(r MetadataApiApiIsAliveRequest) (*Is localVarReturnValue *IsAlive200Response ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataApiService.IsAlive") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataAPIService.IsAlive") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -281,7 +281,7 @@ func (a *MetadataApiService) IsAliveExecute(r MetadataApiApiIsAliveRequest) (*Is return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { @@ -315,12 +315,12 @@ func (a *MetadataApiService) IsAliveExecute(r MetadataApiApiIsAliveRequest) (*Is return localVarReturnValue, localVarHTTPResponse, nil } -type MetadataApiApiIsReadyRequest struct { +type MetadataAPIApiIsReadyRequest struct { ctx context.Context - ApiService MetadataApi + ApiService MetadataAPI } -func (r MetadataApiApiIsReadyRequest) Execute() (*IsAlive200Response, *http.Response, error) { +func (r MetadataAPIApiIsReadyRequest) Execute() (*IsAlive200Response, *http.Response, error) { return r.ApiService.IsReadyExecute(r) } @@ -336,10 +336,10 @@ If the service supports TLS Edge Termination, this endpoint does not require the Be aware that if you are running multiple nodes of Ory Kratos, the health status will never refer to the cluster state, only to a single instance. - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - - @return MetadataApiApiIsReadyRequest + - @return MetadataAPIApiIsReadyRequest */ -func (a *MetadataApiService) IsReady(ctx context.Context) MetadataApiApiIsReadyRequest { - return MetadataApiApiIsReadyRequest{ +func (a *MetadataAPIService) IsReady(ctx context.Context) MetadataAPIApiIsReadyRequest { + return MetadataAPIApiIsReadyRequest{ ApiService: a, ctx: ctx, } @@ -349,7 +349,7 @@ func (a *MetadataApiService) IsReady(ctx context.Context) MetadataApiApiIsReadyR * Execute executes the request * @return IsAlive200Response */ -func (a *MetadataApiService) IsReadyExecute(r MetadataApiApiIsReadyRequest) (*IsAlive200Response, *http.Response, error) { +func (a *MetadataAPIService) IsReadyExecute(r MetadataAPIApiIsReadyRequest) (*IsAlive200Response, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} @@ -359,7 +359,7 @@ func (a *MetadataApiService) IsReadyExecute(r MetadataApiApiIsReadyRequest) (*Is localVarReturnValue *IsAlive200Response ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataApiService.IsReady") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataAPIService.IsReady") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } @@ -397,7 +397,7 @@ func (a *MetadataApiService) IsReadyExecute(r MetadataApiApiIsReadyRequest) (*Is return localVarReturnValue, localVarHTTPResponse, err } - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarBody, err := io.ReadAll(io.LimitReader(localVarHTTPResponse.Body, 1024*1024)) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { diff --git a/internal/httpclient/client.go b/internal/httpclient/client.go index 949a0e51ab35..14ee5d7619a6 100644 --- a/internal/httpclient/client.go +++ b/internal/httpclient/client.go @@ -49,13 +49,13 @@ type APIClient struct { // API Services - CourierApi CourierApi + CourierAPI CourierAPI - FrontendApi FrontendApi + FrontendAPI FrontendAPI - IdentityApi IdentityApi + IdentityAPI IdentityAPI - MetadataApi MetadataApi + MetadataAPI MetadataAPI } type service struct { @@ -74,10 +74,10 @@ func NewAPIClient(cfg *Configuration) *APIClient { c.common.client = c // API Services - c.CourierApi = (*CourierApiService)(&c.common) - c.FrontendApi = (*FrontendApiService)(&c.common) - c.IdentityApi = (*IdentityApiService)(&c.common) - c.MetadataApi = (*MetadataApiService)(&c.common) + c.CourierAPI = (*CourierAPIService)(&c.common) + c.FrontendAPI = (*FrontendAPIService)(&c.common) + c.IdentityAPI = (*IdentityAPIService)(&c.common) + c.MetadataAPI = (*MetadataAPIService)(&c.common) return c } diff --git a/internal/httpclient/model_continue_with.go b/internal/httpclient/model_continue_with.go index ee84e74692fb..6fb1056836e6 100644 --- a/internal/httpclient/model_continue_with.go +++ b/internal/httpclient/model_continue_with.go @@ -19,6 +19,7 @@ import ( // ContinueWith - struct for ContinueWith type ContinueWith struct { ContinueWithRecoveryUi *ContinueWithRecoveryUi + ContinueWithRedirectBrowserTo *ContinueWithRedirectBrowserTo ContinueWithSetOrySessionToken *ContinueWithSetOrySessionToken ContinueWithSettingsUi *ContinueWithSettingsUi ContinueWithVerificationUi *ContinueWithVerificationUi @@ -31,6 +32,13 @@ func ContinueWithRecoveryUiAsContinueWith(v *ContinueWithRecoveryUi) ContinueWit } } +// ContinueWithRedirectBrowserToAsContinueWith is a convenience function that returns ContinueWithRedirectBrowserTo wrapped in ContinueWith +func ContinueWithRedirectBrowserToAsContinueWith(v *ContinueWithRedirectBrowserTo) ContinueWith { + return ContinueWith{ + ContinueWithRedirectBrowserTo: v, + } +} + // ContinueWithSetOrySessionTokenAsContinueWith is a convenience function that returns ContinueWithSetOrySessionToken wrapped in ContinueWith func ContinueWithSetOrySessionTokenAsContinueWith(v *ContinueWithSetOrySessionToken) ContinueWith { return ContinueWith{ @@ -55,72 +63,134 @@ func ContinueWithVerificationUiAsContinueWith(v *ContinueWithVerificationUi) Con // Unmarshal JSON data into one of the pointers in the struct func (dst *ContinueWith) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into ContinueWithRecoveryUi - err = newStrictDecoder(data).Decode(&dst.ContinueWithRecoveryUi) - if err == nil { - jsonContinueWithRecoveryUi, _ := json.Marshal(dst.ContinueWithRecoveryUi) - if string(jsonContinueWithRecoveryUi) == "{}" { // empty struct - dst.ContinueWithRecoveryUi = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'redirect_browser_to' + if jsonDict["action"] == "redirect_browser_to" { + // try to unmarshal JSON data into ContinueWithRedirectBrowserTo + err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo) + if err == nil { + return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match } else { - match++ + dst.ContinueWithRedirectBrowserTo = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error()) } - } else { - dst.ContinueWithRecoveryUi = nil } - // try to unmarshal data into ContinueWithSetOrySessionToken - err = newStrictDecoder(data).Decode(&dst.ContinueWithSetOrySessionToken) - if err == nil { - jsonContinueWithSetOrySessionToken, _ := json.Marshal(dst.ContinueWithSetOrySessionToken) - if string(jsonContinueWithSetOrySessionToken) == "{}" { // empty struct + // check if the discriminator value is 'set_ory_session_token' + if jsonDict["action"] == "set_ory_session_token" { + // try to unmarshal JSON data into ContinueWithSetOrySessionToken + err = json.Unmarshal(data, &dst.ContinueWithSetOrySessionToken) + if err == nil { + return nil // data stored in dst.ContinueWithSetOrySessionToken, return on the first match + } else { dst.ContinueWithSetOrySessionToken = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithSetOrySessionToken: %s", err.Error()) + } + } + + // check if the discriminator value is 'show_recovery_ui' + if jsonDict["action"] == "show_recovery_ui" { + // try to unmarshal JSON data into ContinueWithRecoveryUi + err = json.Unmarshal(data, &dst.ContinueWithRecoveryUi) + if err == nil { + return nil // data stored in dst.ContinueWithRecoveryUi, return on the first match } else { - match++ + dst.ContinueWithRecoveryUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRecoveryUi: %s", err.Error()) } - } else { - dst.ContinueWithSetOrySessionToken = nil } - // try to unmarshal data into ContinueWithSettingsUi - err = newStrictDecoder(data).Decode(&dst.ContinueWithSettingsUi) - if err == nil { - jsonContinueWithSettingsUi, _ := json.Marshal(dst.ContinueWithSettingsUi) - if string(jsonContinueWithSettingsUi) == "{}" { // empty struct - dst.ContinueWithSettingsUi = nil + // check if the discriminator value is 'show_settings_ui' + if jsonDict["action"] == "show_settings_ui" { + // try to unmarshal JSON data into ContinueWithSettingsUi + err = json.Unmarshal(data, &dst.ContinueWithSettingsUi) + if err == nil { + return nil // data stored in dst.ContinueWithSettingsUi, return on the first match } else { - match++ + dst.ContinueWithSettingsUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithSettingsUi: %s", err.Error()) } - } else { - dst.ContinueWithSettingsUi = nil } - // try to unmarshal data into ContinueWithVerificationUi - err = newStrictDecoder(data).Decode(&dst.ContinueWithVerificationUi) - if err == nil { - jsonContinueWithVerificationUi, _ := json.Marshal(dst.ContinueWithVerificationUi) - if string(jsonContinueWithVerificationUi) == "{}" { // empty struct + // check if the discriminator value is 'show_verification_ui' + if jsonDict["action"] == "show_verification_ui" { + // try to unmarshal JSON data into ContinueWithVerificationUi + err = json.Unmarshal(data, &dst.ContinueWithVerificationUi) + if err == nil { + return nil // data stored in dst.ContinueWithVerificationUi, return on the first match + } else { dst.ContinueWithVerificationUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithVerificationUi: %s", err.Error()) + } + } + + // check if the discriminator value is 'continueWithRecoveryUi' + if jsonDict["action"] == "continueWithRecoveryUi" { + // try to unmarshal JSON data into ContinueWithRecoveryUi + err = json.Unmarshal(data, &dst.ContinueWithRecoveryUi) + if err == nil { + return nil // data stored in dst.ContinueWithRecoveryUi, return on the first match } else { - match++ + dst.ContinueWithRecoveryUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRecoveryUi: %s", err.Error()) } - } else { - dst.ContinueWithVerificationUi = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.ContinueWithRecoveryUi = nil - dst.ContinueWithSetOrySessionToken = nil - dst.ContinueWithSettingsUi = nil - dst.ContinueWithVerificationUi = nil + // check if the discriminator value is 'continueWithRedirectBrowserTo' + if jsonDict["action"] == "continueWithRedirectBrowserTo" { + // try to unmarshal JSON data into ContinueWithRedirectBrowserTo + err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo) + if err == nil { + return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match + } else { + dst.ContinueWithRedirectBrowserTo = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error()) + } + } + + // check if the discriminator value is 'continueWithSetOrySessionToken' + if jsonDict["action"] == "continueWithSetOrySessionToken" { + // try to unmarshal JSON data into ContinueWithSetOrySessionToken + err = json.Unmarshal(data, &dst.ContinueWithSetOrySessionToken) + if err == nil { + return nil // data stored in dst.ContinueWithSetOrySessionToken, return on the first match + } else { + dst.ContinueWithSetOrySessionToken = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithSetOrySessionToken: %s", err.Error()) + } + } + + // check if the discriminator value is 'continueWithSettingsUi' + if jsonDict["action"] == "continueWithSettingsUi" { + // try to unmarshal JSON data into ContinueWithSettingsUi + err = json.Unmarshal(data, &dst.ContinueWithSettingsUi) + if err == nil { + return nil // data stored in dst.ContinueWithSettingsUi, return on the first match + } else { + dst.ContinueWithSettingsUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithSettingsUi: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(ContinueWith)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(ContinueWith)") + // check if the discriminator value is 'continueWithVerificationUi' + if jsonDict["action"] == "continueWithVerificationUi" { + // try to unmarshal JSON data into ContinueWithVerificationUi + err = json.Unmarshal(data, &dst.ContinueWithVerificationUi) + if err == nil { + return nil // data stored in dst.ContinueWithVerificationUi, return on the first match + } else { + dst.ContinueWithVerificationUi = nil + return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithVerificationUi: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON @@ -129,6 +199,10 @@ func (src ContinueWith) MarshalJSON() ([]byte, error) { return json.Marshal(&src.ContinueWithRecoveryUi) } + if src.ContinueWithRedirectBrowserTo != nil { + return json.Marshal(&src.ContinueWithRedirectBrowserTo) + } + if src.ContinueWithSetOrySessionToken != nil { return json.Marshal(&src.ContinueWithSetOrySessionToken) } @@ -153,6 +227,10 @@ func (obj *ContinueWith) GetActualInstance() interface{} { return obj.ContinueWithRecoveryUi } + if obj.ContinueWithRedirectBrowserTo != nil { + return obj.ContinueWithRedirectBrowserTo + } + if obj.ContinueWithSetOrySessionToken != nil { return obj.ContinueWithSetOrySessionToken } diff --git a/internal/httpclient/model_continue_with_recovery_ui_flow.go b/internal/httpclient/model_continue_with_recovery_ui_flow.go index 3fde7e717ef2..251725a73c3b 100644 --- a/internal/httpclient/model_continue_with_recovery_ui_flow.go +++ b/internal/httpclient/model_continue_with_recovery_ui_flow.go @@ -19,7 +19,7 @@ import ( type ContinueWithRecoveryUiFlow struct { // The ID of the recovery flow Id string `json:"id"` - // The URL of the recovery flow + // The URL of the recovery flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. Url *string `json:"url,omitempty"` } diff --git a/internal/httpclient/model_continue_with_redirect_browser_to.go b/internal/httpclient/model_continue_with_redirect_browser_to.go new file mode 100644 index 000000000000..20c3e4f3c562 --- /dev/null +++ b/internal/httpclient/model_continue_with_redirect_browser_to.go @@ -0,0 +1,138 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// ContinueWithRedirectBrowserTo Indicates, that the UI flow could be continued by showing a recovery ui +type ContinueWithRedirectBrowserTo struct { + // Action will always be `redirect_browser_to` redirect_browser_to ContinueWithActionRedirectBrowserToString + Action string `json:"action"` + // The URL to redirect the browser to + RedirectBrowserTo string `json:"redirect_browser_to"` +} + +// NewContinueWithRedirectBrowserTo instantiates a new ContinueWithRedirectBrowserTo object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewContinueWithRedirectBrowserTo(action string, redirectBrowserTo string) *ContinueWithRedirectBrowserTo { + this := ContinueWithRedirectBrowserTo{} + this.Action = action + this.RedirectBrowserTo = redirectBrowserTo + return &this +} + +// NewContinueWithRedirectBrowserToWithDefaults instantiates a new ContinueWithRedirectBrowserTo object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewContinueWithRedirectBrowserToWithDefaults() *ContinueWithRedirectBrowserTo { + this := ContinueWithRedirectBrowserTo{} + return &this +} + +// GetAction returns the Action field value +func (o *ContinueWithRedirectBrowserTo) GetAction() string { + if o == nil { + var ret string + return ret + } + + return o.Action +} + +// GetActionOk returns a tuple with the Action field value +// and a boolean to check if the value has been set. +func (o *ContinueWithRedirectBrowserTo) GetActionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Action, true +} + +// SetAction sets field value +func (o *ContinueWithRedirectBrowserTo) SetAction(v string) { + o.Action = v +} + +// GetRedirectBrowserTo returns the RedirectBrowserTo field value +func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserTo() string { + if o == nil { + var ret string + return ret + } + + return o.RedirectBrowserTo +} + +// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value +// and a boolean to check if the value has been set. +func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserToOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.RedirectBrowserTo, true +} + +// SetRedirectBrowserTo sets field value +func (o *ContinueWithRedirectBrowserTo) SetRedirectBrowserTo(v string) { + o.RedirectBrowserTo = v +} + +func (o ContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if true { + toSerialize["action"] = o.Action + } + if true { + toSerialize["redirect_browser_to"] = o.RedirectBrowserTo + } + return json.Marshal(toSerialize) +} + +type NullableContinueWithRedirectBrowserTo struct { + value *ContinueWithRedirectBrowserTo + isSet bool +} + +func (v NullableContinueWithRedirectBrowserTo) Get() *ContinueWithRedirectBrowserTo { + return v.value +} + +func (v *NullableContinueWithRedirectBrowserTo) Set(val *ContinueWithRedirectBrowserTo) { + v.value = val + v.isSet = true +} + +func (v NullableContinueWithRedirectBrowserTo) IsSet() bool { + return v.isSet +} + +func (v *NullableContinueWithRedirectBrowserTo) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableContinueWithRedirectBrowserTo(val *ContinueWithRedirectBrowserTo) *NullableContinueWithRedirectBrowserTo { + return &NullableContinueWithRedirectBrowserTo{value: val, isSet: true} +} + +func (v NullableContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableContinueWithRedirectBrowserTo) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_continue_with_settings_ui_flow.go b/internal/httpclient/model_continue_with_settings_ui_flow.go index 4ccaf74ef1b8..d6e9b9441f99 100644 --- a/internal/httpclient/model_continue_with_settings_ui_flow.go +++ b/internal/httpclient/model_continue_with_settings_ui_flow.go @@ -19,6 +19,8 @@ import ( type ContinueWithSettingsUiFlow struct { // The ID of the settings flow Id string `json:"id"` + // The URL of the settings flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. + Url *string `json:"url,omitempty"` } // NewContinueWithSettingsUiFlow instantiates a new ContinueWithSettingsUiFlow object @@ -63,11 +65,46 @@ func (o *ContinueWithSettingsUiFlow) SetId(v string) { o.Id = v } +// GetUrl returns the Url field value if set, zero value otherwise. +func (o *ContinueWithSettingsUiFlow) GetUrl() string { + if o == nil || o.Url == nil { + var ret string + return ret + } + return *o.Url +} + +// GetUrlOk returns a tuple with the Url field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ContinueWithSettingsUiFlow) GetUrlOk() (*string, bool) { + if o == nil || o.Url == nil { + return nil, false + } + return o.Url, true +} + +// HasUrl returns a boolean if a field has been set. +func (o *ContinueWithSettingsUiFlow) HasUrl() bool { + if o != nil && o.Url != nil { + return true + } + + return false +} + +// SetUrl gets a reference to the given string and assigns it to the Url field. +func (o *ContinueWithSettingsUiFlow) SetUrl(v string) { + o.Url = &v +} + func (o ContinueWithSettingsUiFlow) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if true { toSerialize["id"] = o.Id } + if o.Url != nil { + toSerialize["url"] = o.Url + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_continue_with_verification_ui_flow.go b/internal/httpclient/model_continue_with_verification_ui_flow.go index 8fdd4609cf93..3c73a0761339 100644 --- a/internal/httpclient/model_continue_with_verification_ui_flow.go +++ b/internal/httpclient/model_continue_with_verification_ui_flow.go @@ -19,7 +19,7 @@ import ( type ContinueWithVerificationUiFlow struct { // The ID of the verification flow Id string `json:"id"` - // The URL of the verification flow + // The URL of the verification flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. Url *string `json:"url,omitempty"` // The address that should be verified in this flow VerifiableAddress string `json:"verifiable_address"` diff --git a/internal/httpclient/model_create_recovery_code_for_identity_body.go b/internal/httpclient/model_create_recovery_code_for_identity_body.go index 850c7e086206..2947fad34e51 100644 --- a/internal/httpclient/model_create_recovery_code_for_identity_body.go +++ b/internal/httpclient/model_create_recovery_code_for_identity_body.go @@ -19,6 +19,8 @@ import ( type CreateRecoveryCodeForIdentityBody struct { // Code Expires In The recovery code will expire after that amount of time has passed. Defaults to the configuration value of `selfservice.methods.code.config.lifespan`. ExpiresIn *string `json:"expires_in,omitempty"` + // The flow type can either be `api` or `browser`. + FlowType *string `json:"flow_type,omitempty"` // Identity to Recover The identity's ID you wish to recover. IdentityId string `json:"identity_id"` } @@ -73,6 +75,38 @@ func (o *CreateRecoveryCodeForIdentityBody) SetExpiresIn(v string) { o.ExpiresIn = &v } +// GetFlowType returns the FlowType field value if set, zero value otherwise. +func (o *CreateRecoveryCodeForIdentityBody) GetFlowType() string { + if o == nil || o.FlowType == nil { + var ret string + return ret + } + return *o.FlowType +} + +// GetFlowTypeOk returns a tuple with the FlowType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateRecoveryCodeForIdentityBody) GetFlowTypeOk() (*string, bool) { + if o == nil || o.FlowType == nil { + return nil, false + } + return o.FlowType, true +} + +// HasFlowType returns a boolean if a field has been set. +func (o *CreateRecoveryCodeForIdentityBody) HasFlowType() bool { + if o != nil && o.FlowType != nil { + return true + } + + return false +} + +// SetFlowType gets a reference to the given string and assigns it to the FlowType field. +func (o *CreateRecoveryCodeForIdentityBody) SetFlowType(v string) { + o.FlowType = &v +} + // GetIdentityId returns the IdentityId field value func (o *CreateRecoveryCodeForIdentityBody) GetIdentityId() string { if o == nil { @@ -102,6 +136,9 @@ func (o CreateRecoveryCodeForIdentityBody) MarshalJSON() ([]byte, error) { if o.ExpiresIn != nil { toSerialize["expires_in"] = o.ExpiresIn } + if o.FlowType != nil { + toSerialize["flow_type"] = o.FlowType + } if true { toSerialize["identity_id"] = o.IdentityId } diff --git a/internal/httpclient/model_identity_credentials.go b/internal/httpclient/model_identity_credentials.go index 5b454383d520..7ee96800df4b 100644 --- a/internal/httpclient/model_identity_credentials.go +++ b/internal/httpclient/model_identity_credentials.go @@ -23,7 +23,7 @@ type IdentityCredentials struct { CreatedAt *time.Time `json:"created_at,omitempty"` // Identifiers represents a list of unique identifiers this credential type matches. Identifiers []string `json:"identifiers,omitempty"` - // Type discriminates between different types of credentials. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + // Type discriminates between different types of credentials. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode Type *string `json:"type,omitempty"` // UpdatedAt is a helper struct field for gobuffalo.pop. UpdatedAt *time.Time `json:"updated_at,omitempty"` diff --git a/internal/httpclient/model_identity_credentials_code.go b/internal/httpclient/model_identity_credentials_code.go index 75857f31c272..53fefb6719eb 100644 --- a/internal/httpclient/model_identity_credentials_code.go +++ b/internal/httpclient/model_identity_credentials_code.go @@ -13,14 +13,11 @@ package client import ( "encoding/json" - "time" ) // IdentityCredentialsCode CredentialsCode represents a one time login/registration code type IdentityCredentialsCode struct { - // The type of the address for this code - AddressType *string `json:"address_type,omitempty"` - UsedAt NullableTime `json:"used_at,omitempty"` + Addresses []IdentityCredentialsCodeAddress `json:"addresses,omitempty"` } // NewIdentityCredentialsCode instantiates a new IdentityCredentialsCode object @@ -40,88 +37,42 @@ func NewIdentityCredentialsCodeWithDefaults() *IdentityCredentialsCode { return &this } -// GetAddressType returns the AddressType field value if set, zero value otherwise. -func (o *IdentityCredentialsCode) GetAddressType() string { - if o == nil || o.AddressType == nil { - var ret string +// GetAddresses returns the Addresses field value if set, zero value otherwise. +func (o *IdentityCredentialsCode) GetAddresses() []IdentityCredentialsCodeAddress { + if o == nil || o.Addresses == nil { + var ret []IdentityCredentialsCodeAddress return ret } - return *o.AddressType + return o.Addresses } -// GetAddressTypeOk returns a tuple with the AddressType field value if set, nil otherwise +// GetAddressesOk returns a tuple with the Addresses field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *IdentityCredentialsCode) GetAddressTypeOk() (*string, bool) { - if o == nil || o.AddressType == nil { +func (o *IdentityCredentialsCode) GetAddressesOk() ([]IdentityCredentialsCodeAddress, bool) { + if o == nil || o.Addresses == nil { return nil, false } - return o.AddressType, true + return o.Addresses, true } -// HasAddressType returns a boolean if a field has been set. -func (o *IdentityCredentialsCode) HasAddressType() bool { - if o != nil && o.AddressType != nil { +// HasAddresses returns a boolean if a field has been set. +func (o *IdentityCredentialsCode) HasAddresses() bool { + if o != nil && o.Addresses != nil { return true } return false } -// SetAddressType gets a reference to the given string and assigns it to the AddressType field. -func (o *IdentityCredentialsCode) SetAddressType(v string) { - o.AddressType = &v -} - -// GetUsedAt returns the UsedAt field value if set, zero value otherwise (both if not set or set to explicit null). -func (o *IdentityCredentialsCode) GetUsedAt() time.Time { - if o == nil || o.UsedAt.Get() == nil { - var ret time.Time - return ret - } - return *o.UsedAt.Get() -} - -// GetUsedAtOk returns a tuple with the UsedAt field value if set, nil otherwise -// and a boolean to check if the value has been set. -// NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *IdentityCredentialsCode) GetUsedAtOk() (*time.Time, bool) { - if o == nil { - return nil, false - } - return o.UsedAt.Get(), o.UsedAt.IsSet() -} - -// HasUsedAt returns a boolean if a field has been set. -func (o *IdentityCredentialsCode) HasUsedAt() bool { - if o != nil && o.UsedAt.IsSet() { - return true - } - - return false -} - -// SetUsedAt gets a reference to the given NullableTime and assigns it to the UsedAt field. -func (o *IdentityCredentialsCode) SetUsedAt(v time.Time) { - o.UsedAt.Set(&v) -} - -// SetUsedAtNil sets the value for UsedAt to be an explicit nil -func (o *IdentityCredentialsCode) SetUsedAtNil() { - o.UsedAt.Set(nil) -} - -// UnsetUsedAt ensures that no value is present for UsedAt, not even an explicit nil -func (o *IdentityCredentialsCode) UnsetUsedAt() { - o.UsedAt.Unset() +// SetAddresses gets a reference to the given []IdentityCredentialsCodeAddress and assigns it to the Addresses field. +func (o *IdentityCredentialsCode) SetAddresses(v []IdentityCredentialsCodeAddress) { + o.Addresses = v } func (o IdentityCredentialsCode) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} - if o.AddressType != nil { - toSerialize["address_type"] = o.AddressType - } - if o.UsedAt.IsSet() { - toSerialize["used_at"] = o.UsedAt.Get() + if o.Addresses != nil { + toSerialize["addresses"] = o.Addresses } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_identity_credentials_code_address.go b/internal/httpclient/model_identity_credentials_code_address.go new file mode 100644 index 000000000000..c739045e79e0 --- /dev/null +++ b/internal/httpclient/model_identity_credentials_code_address.go @@ -0,0 +1,151 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// IdentityCredentialsCodeAddress struct for IdentityCredentialsCodeAddress +type IdentityCredentialsCodeAddress struct { + // The address for this code + Address *string `json:"address,omitempty"` + Channel *string `json:"channel,omitempty"` +} + +// NewIdentityCredentialsCodeAddress instantiates a new IdentityCredentialsCodeAddress object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewIdentityCredentialsCodeAddress() *IdentityCredentialsCodeAddress { + this := IdentityCredentialsCodeAddress{} + return &this +} + +// NewIdentityCredentialsCodeAddressWithDefaults instantiates a new IdentityCredentialsCodeAddress object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewIdentityCredentialsCodeAddressWithDefaults() *IdentityCredentialsCodeAddress { + this := IdentityCredentialsCodeAddress{} + return &this +} + +// GetAddress returns the Address field value if set, zero value otherwise. +func (o *IdentityCredentialsCodeAddress) GetAddress() string { + if o == nil || o.Address == nil { + var ret string + return ret + } + return *o.Address +} + +// GetAddressOk returns a tuple with the Address field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsCodeAddress) GetAddressOk() (*string, bool) { + if o == nil || o.Address == nil { + return nil, false + } + return o.Address, true +} + +// HasAddress returns a boolean if a field has been set. +func (o *IdentityCredentialsCodeAddress) HasAddress() bool { + if o != nil && o.Address != nil { + return true + } + + return false +} + +// SetAddress gets a reference to the given string and assigns it to the Address field. +func (o *IdentityCredentialsCodeAddress) SetAddress(v string) { + o.Address = &v +} + +// GetChannel returns the Channel field value if set, zero value otherwise. +func (o *IdentityCredentialsCodeAddress) GetChannel() string { + if o == nil || o.Channel == nil { + var ret string + return ret + } + return *o.Channel +} + +// GetChannelOk returns a tuple with the Channel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsCodeAddress) GetChannelOk() (*string, bool) { + if o == nil || o.Channel == nil { + return nil, false + } + return o.Channel, true +} + +// HasChannel returns a boolean if a field has been set. +func (o *IdentityCredentialsCodeAddress) HasChannel() bool { + if o != nil && o.Channel != nil { + return true + } + + return false +} + +// SetChannel gets a reference to the given string and assigns it to the Channel field. +func (o *IdentityCredentialsCodeAddress) SetChannel(v string) { + o.Channel = &v +} + +func (o IdentityCredentialsCodeAddress) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Address != nil { + toSerialize["address"] = o.Address + } + if o.Channel != nil { + toSerialize["channel"] = o.Channel + } + return json.Marshal(toSerialize) +} + +type NullableIdentityCredentialsCodeAddress struct { + value *IdentityCredentialsCodeAddress + isSet bool +} + +func (v NullableIdentityCredentialsCodeAddress) Get() *IdentityCredentialsCodeAddress { + return v.value +} + +func (v *NullableIdentityCredentialsCodeAddress) Set(val *IdentityCredentialsCodeAddress) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsCodeAddress) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsCodeAddress) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsCodeAddress(val *IdentityCredentialsCodeAddress) *NullableIdentityCredentialsCodeAddress { + return &NullableIdentityCredentialsCodeAddress{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsCodeAddress) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsCodeAddress) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_identity_credentials_password.go b/internal/httpclient/model_identity_credentials_password.go index 85f942fb6852..df1900568bb3 100644 --- a/internal/httpclient/model_identity_credentials_password.go +++ b/internal/httpclient/model_identity_credentials_password.go @@ -19,6 +19,8 @@ import ( type IdentityCredentialsPassword struct { // HashedPassword is a hash-representation of the password. HashedPassword *string `json:"hashed_password,omitempty"` + // UsePasswordMigrationHook is set to true if the password should be migrated using the password migration hook. If set, and the HashedPassword is empty, a webhook will be called during login to migrate the password. + UsePasswordMigrationHook *bool `json:"use_password_migration_hook,omitempty"` } // NewIdentityCredentialsPassword instantiates a new IdentityCredentialsPassword object @@ -70,11 +72,46 @@ func (o *IdentityCredentialsPassword) SetHashedPassword(v string) { o.HashedPassword = &v } +// GetUsePasswordMigrationHook returns the UsePasswordMigrationHook field value if set, zero value otherwise. +func (o *IdentityCredentialsPassword) GetUsePasswordMigrationHook() bool { + if o == nil || o.UsePasswordMigrationHook == nil { + var ret bool + return ret + } + return *o.UsePasswordMigrationHook +} + +// GetUsePasswordMigrationHookOk returns a tuple with the UsePasswordMigrationHook field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsPassword) GetUsePasswordMigrationHookOk() (*bool, bool) { + if o == nil || o.UsePasswordMigrationHook == nil { + return nil, false + } + return o.UsePasswordMigrationHook, true +} + +// HasUsePasswordMigrationHook returns a boolean if a field has been set. +func (o *IdentityCredentialsPassword) HasUsePasswordMigrationHook() bool { + if o != nil && o.UsePasswordMigrationHook != nil { + return true + } + + return false +} + +// SetUsePasswordMigrationHook gets a reference to the given bool and assigns it to the UsePasswordMigrationHook field. +func (o *IdentityCredentialsPassword) SetUsePasswordMigrationHook(v bool) { + o.UsePasswordMigrationHook = &v +} + func (o IdentityCredentialsPassword) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.HashedPassword != nil { toSerialize["hashed_password"] = o.HashedPassword } + if o.UsePasswordMigrationHook != nil { + toSerialize["use_password_migration_hook"] = o.UsePasswordMigrationHook + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_identity_patch_response.go b/internal/httpclient/model_identity_patch_response.go index 2ee305f7da81..f67224edad01 100644 --- a/internal/httpclient/model_identity_patch_response.go +++ b/internal/httpclient/model_identity_patch_response.go @@ -17,8 +17,9 @@ import ( // IdentityPatchResponse Response for a single identity patch type IdentityPatchResponse struct { - // The action for this specific patch create ActionCreate Create this identity. - Action *string `json:"action,omitempty"` + // The action for this specific patch create ActionCreate Create this identity. error ActionError Error indicates that the patch failed. + Action *string `json:"action,omitempty"` + Error interface{} `json:"error,omitempty"` // The identity ID payload of this patch Identity *string `json:"identity,omitempty"` // The ID of this patch response, if an ID was specified in the patch. @@ -74,6 +75,39 @@ func (o *IdentityPatchResponse) SetAction(v string) { o.Action = &v } +// GetError returns the Error field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *IdentityPatchResponse) GetError() interface{} { + if o == nil { + var ret interface{} + return ret + } + return o.Error +} + +// GetErrorOk returns a tuple with the Error field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *IdentityPatchResponse) GetErrorOk() (*interface{}, bool) { + if o == nil || o.Error == nil { + return nil, false + } + return &o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *IdentityPatchResponse) HasError() bool { + if o != nil && o.Error != nil { + return true + } + + return false +} + +// SetError gets a reference to the given interface{} and assigns it to the Error field. +func (o *IdentityPatchResponse) SetError(v interface{}) { + o.Error = v +} + // GetIdentity returns the Identity field value if set, zero value otherwise. func (o *IdentityPatchResponse) GetIdentity() string { if o == nil || o.Identity == nil { @@ -143,6 +177,9 @@ func (o IdentityPatchResponse) MarshalJSON() ([]byte, error) { if o.Action != nil { toSerialize["action"] = o.Action } + if o.Error != nil { + toSerialize["error"] = o.Error + } if o.Identity != nil { toSerialize["identity"] = o.Identity } diff --git a/internal/httpclient/model_identity_with_credentials_password_config.go b/internal/httpclient/model_identity_with_credentials_password_config.go index 754d59460f83..34f09ae58232 100644 --- a/internal/httpclient/model_identity_with_credentials_password_config.go +++ b/internal/httpclient/model_identity_with_credentials_password_config.go @@ -21,6 +21,8 @@ type IdentityWithCredentialsPasswordConfig struct { HashedPassword *string `json:"hashed_password,omitempty"` // The password in plain text if no hash is available. Password *string `json:"password,omitempty"` + // If set to true, the password will be migrated using the password migration hook. + UsePasswordMigrationHook *bool `json:"use_password_migration_hook,omitempty"` } // NewIdentityWithCredentialsPasswordConfig instantiates a new IdentityWithCredentialsPasswordConfig object @@ -104,6 +106,38 @@ func (o *IdentityWithCredentialsPasswordConfig) SetPassword(v string) { o.Password = &v } +// GetUsePasswordMigrationHook returns the UsePasswordMigrationHook field value if set, zero value otherwise. +func (o *IdentityWithCredentialsPasswordConfig) GetUsePasswordMigrationHook() bool { + if o == nil || o.UsePasswordMigrationHook == nil { + var ret bool + return ret + } + return *o.UsePasswordMigrationHook +} + +// GetUsePasswordMigrationHookOk returns a tuple with the UsePasswordMigrationHook field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityWithCredentialsPasswordConfig) GetUsePasswordMigrationHookOk() (*bool, bool) { + if o == nil || o.UsePasswordMigrationHook == nil { + return nil, false + } + return o.UsePasswordMigrationHook, true +} + +// HasUsePasswordMigrationHook returns a boolean if a field has been set. +func (o *IdentityWithCredentialsPasswordConfig) HasUsePasswordMigrationHook() bool { + if o != nil && o.UsePasswordMigrationHook != nil { + return true + } + + return false +} + +// SetUsePasswordMigrationHook gets a reference to the given bool and assigns it to the UsePasswordMigrationHook field. +func (o *IdentityWithCredentialsPasswordConfig) SetUsePasswordMigrationHook(v bool) { + o.UsePasswordMigrationHook = &v +} + func (o IdentityWithCredentialsPasswordConfig) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.HashedPassword != nil { @@ -112,6 +146,9 @@ func (o IdentityWithCredentialsPasswordConfig) MarshalJSON() ([]byte, error) { if o.Password != nil { toSerialize["password"] = o.Password } + if o.UsePasswordMigrationHook != nil { + toSerialize["use_password_migration_hook"] = o.UsePasswordMigrationHook + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_login_flow.go b/internal/httpclient/model_login_flow.go index b36abc379568..2794adee0b83 100644 --- a/internal/httpclient/model_login_flow.go +++ b/internal/httpclient/model_login_flow.go @@ -18,7 +18,7 @@ import ( // LoginFlow This object represents a login flow. A login flow is initiated at the \"Initiate Login API / Browser Flow\" endpoint by a client. Once a login flow is completed successfully, a session cookie or session token will be issued. type LoginFlow struct { - // The active login method If set contains the login method used. If the flow is new, it is unset. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + // The active login method If set contains the login method used. If the flow is new, it is unset. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode Active *string `json:"active,omitempty"` // CreatedAt is a helper struct field for gobuffalo.pop. CreatedAt *time.Time `json:"created_at,omitempty"` @@ -43,6 +43,8 @@ type LoginFlow struct { SessionTokenExchangeCode *string `json:"session_token_exchange_code,omitempty"` // State represents the state of this request: choose_method: ask the user to choose a method to sign in with sent_email: the email has been sent to the user passed_challenge: the request was successful and the login challenge was passed. State interface{} `json:"state"` + // TransientPayload is used to pass data from the login to hooks and email templates + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -495,6 +497,38 @@ func (o *LoginFlow) SetState(v interface{}) { o.State = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *LoginFlow) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *LoginFlow) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *LoginFlow) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *LoginFlow) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetType returns the Type field value func (o *LoginFlow) GetType() string { if o == nil { @@ -619,6 +653,9 @@ func (o LoginFlow) MarshalJSON() ([]byte, error) { if o.State != nil { toSerialize["state"] = o.State } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if true { toSerialize["type"] = o.Type } diff --git a/internal/httpclient/model_login_flow_state.go b/internal/httpclient/model_login_flow_state.go index ce5570b79032..58af057c612f 100644 --- a/internal/httpclient/model_login_flow_state.go +++ b/internal/httpclient/model_login_flow_state.go @@ -16,7 +16,7 @@ import ( "fmt" ) -// LoginFlowState The state represents the state of the login flow. choose_method: ask the user to choose a method (e.g. login account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the login challenge was passed. +// LoginFlowState The experimental state represents the state of a login flow. This field is EXPERIMENTAL and subject to change! type LoginFlowState string // List of loginFlowState diff --git a/internal/httpclient/model_o_auth2_client.go b/internal/httpclient/model_o_auth2_client.go index be48d3217ade..bbf75b334104 100644 --- a/internal/httpclient/model_o_auth2_client.go +++ b/internal/httpclient/model_o_auth2_client.go @@ -18,6 +18,7 @@ import ( // OAuth2Client struct for OAuth2Client type OAuth2Client struct { + AdditionalPropertiesField map[string]interface{} `json:"AdditionalProperties,omitempty"` // OAuth 2.0 Access Token Strategy AccessTokenStrategy is the strategy used to generate access tokens. Valid options are `jwt` and `opaque`. `jwt` is a bad idea, see https://www.ory.sh/docs/hydra/advanced#json-web-tokens Setting the stragegy here overrides the global setting in `strategies.access_token`. AccessTokenStrategy *string `json:"access_token_strategy,omitempty"` AllowedCorsOrigins []string `json:"allowed_cors_origins,omitempty"` @@ -124,6 +125,38 @@ func NewOAuth2ClientWithDefaults() *OAuth2Client { return &this } +// GetAdditionalPropertiesField returns the AdditionalPropertiesField field value if set, zero value otherwise. +func (o *OAuth2Client) GetAdditionalPropertiesField() map[string]interface{} { + if o == nil || o.AdditionalPropertiesField == nil { + var ret map[string]interface{} + return ret + } + return o.AdditionalPropertiesField +} + +// GetAdditionalPropertiesFieldOk returns a tuple with the AdditionalPropertiesField field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2Client) GetAdditionalPropertiesFieldOk() (map[string]interface{}, bool) { + if o == nil || o.AdditionalPropertiesField == nil { + return nil, false + } + return o.AdditionalPropertiesField, true +} + +// HasAdditionalPropertiesField returns a boolean if a field has been set. +func (o *OAuth2Client) HasAdditionalPropertiesField() bool { + if o != nil && o.AdditionalPropertiesField != nil { + return true + } + + return false +} + +// SetAdditionalPropertiesField gets a reference to the given map[string]interface{} and assigns it to the AdditionalPropertiesField field. +func (o *OAuth2Client) SetAdditionalPropertiesField(v map[string]interface{}) { + o.AdditionalPropertiesField = v +} + // GetAccessTokenStrategy returns the AccessTokenStrategy field value if set, zero value otherwise. func (o *OAuth2Client) GetAccessTokenStrategy() string { if o == nil || o.AccessTokenStrategy == nil { @@ -1664,6 +1697,9 @@ func (o *OAuth2Client) SetUserinfoSignedResponseAlg(v string) { func (o OAuth2Client) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} + if o.AdditionalPropertiesField != nil { + toSerialize["AdditionalProperties"] = o.AdditionalPropertiesField + } if o.AccessTokenStrategy != nil { toSerialize["access_token_strategy"] = o.AccessTokenStrategy } diff --git a/internal/httpclient/model_o_auth2_consent_request_open_id_connect_context.go b/internal/httpclient/model_o_auth2_consent_request_open_id_connect_context.go index c0cbf7f3129e..68884830a9ee 100644 --- a/internal/httpclient/model_o_auth2_consent_request_open_id_connect_context.go +++ b/internal/httpclient/model_o_auth2_consent_request_open_id_connect_context.go @@ -17,6 +17,7 @@ import ( // OAuth2ConsentRequestOpenIDConnectContext OAuth2ConsentRequestOpenIDConnectContext struct for OAuth2ConsentRequestOpenIDConnectContext type OAuth2ConsentRequestOpenIDConnectContext struct { + AdditionalPropertiesField map[string]interface{} `json:"AdditionalProperties,omitempty"` // ACRValues is the Authentication AuthorizationContext Class Reference requested in the OAuth 2.0 Authorization request. It is a parameter defined by OpenID Connect and expresses which level of authentication (e.g. 2FA) is required. OpenID Connect defines it as follows: > Requested Authentication AuthorizationContext Class Reference values. Space-separated string that specifies the acr values that the Authorization Server is being requested to use for processing this Authentication Request, with the values appearing in order of preference. The Authentication AuthorizationContext Class satisfied by the authentication performed is returned as the acr Claim Value, as specified in Section 2. The acr Claim is requested as a Voluntary Claim by this parameter. AcrValues []string `json:"acr_values,omitempty"` // Display is a string value that specifies how the Authorization Server displays the authentication and consent user interface pages to the End-User. The defined values are: page: The Authorization Server SHOULD display the authentication and consent UI consistent with a full User Agent page view. If the display parameter is not specified, this is the default display mode. popup: The Authorization Server SHOULD display the authentication and consent UI consistent with a popup User Agent window. The popup User Agent window should be of an appropriate size for a login-focused dialog and should not obscure the entire window that it is popping up over. touch: The Authorization Server SHOULD display the authentication and consent UI consistent with a device that leverages a touch interface. wap: The Authorization Server SHOULD display the authentication and consent UI consistent with a \\\"feature phone\\\" type display. The Authorization Server MAY also attempt to detect the capabilities of the User Agent and present an appropriate display. @@ -46,6 +47,38 @@ func NewOAuth2ConsentRequestOpenIDConnectContextWithDefaults() *OAuth2ConsentReq return &this } +// GetAdditionalPropertiesField returns the AdditionalPropertiesField field value if set, zero value otherwise. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetAdditionalPropertiesField() map[string]interface{} { + if o == nil || o.AdditionalPropertiesField == nil { + var ret map[string]interface{} + return ret + } + return o.AdditionalPropertiesField +} + +// GetAdditionalPropertiesFieldOk returns a tuple with the AdditionalPropertiesField field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) GetAdditionalPropertiesFieldOk() (map[string]interface{}, bool) { + if o == nil || o.AdditionalPropertiesField == nil { + return nil, false + } + return o.AdditionalPropertiesField, true +} + +// HasAdditionalPropertiesField returns a boolean if a field has been set. +func (o *OAuth2ConsentRequestOpenIDConnectContext) HasAdditionalPropertiesField() bool { + if o != nil && o.AdditionalPropertiesField != nil { + return true + } + + return false +} + +// SetAdditionalPropertiesField gets a reference to the given map[string]interface{} and assigns it to the AdditionalPropertiesField field. +func (o *OAuth2ConsentRequestOpenIDConnectContext) SetAdditionalPropertiesField(v map[string]interface{}) { + o.AdditionalPropertiesField = v +} + // GetAcrValues returns the AcrValues field value if set, zero value otherwise. func (o *OAuth2ConsentRequestOpenIDConnectContext) GetAcrValues() []string { if o == nil || o.AcrValues == nil { @@ -208,6 +241,9 @@ func (o *OAuth2ConsentRequestOpenIDConnectContext) SetUiLocales(v []string) { func (o OAuth2ConsentRequestOpenIDConnectContext) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} + if o.AdditionalPropertiesField != nil { + toSerialize["AdditionalProperties"] = o.AdditionalPropertiesField + } if o.AcrValues != nil { toSerialize["acr_values"] = o.AcrValues } diff --git a/internal/httpclient/model_o_auth2_login_request.go b/internal/httpclient/model_o_auth2_login_request.go index 9fcd87be72fa..a788c66ed3ca 100644 --- a/internal/httpclient/model_o_auth2_login_request.go +++ b/internal/httpclient/model_o_auth2_login_request.go @@ -17,6 +17,7 @@ import ( // OAuth2LoginRequest OAuth2LoginRequest struct for OAuth2LoginRequest type OAuth2LoginRequest struct { + AdditionalPropertiesField map[string]interface{} `json:"AdditionalProperties,omitempty"` // ID is the identifier (\\\"login challenge\\\") of the login request. It is used to identify the session. Challenge *string `json:"challenge,omitempty"` Client *OAuth2Client `json:"client,omitempty"` @@ -50,6 +51,38 @@ func NewOAuth2LoginRequestWithDefaults() *OAuth2LoginRequest { return &this } +// GetAdditionalPropertiesField returns the AdditionalPropertiesField field value if set, zero value otherwise. +func (o *OAuth2LoginRequest) GetAdditionalPropertiesField() map[string]interface{} { + if o == nil || o.AdditionalPropertiesField == nil { + var ret map[string]interface{} + return ret + } + return o.AdditionalPropertiesField +} + +// GetAdditionalPropertiesFieldOk returns a tuple with the AdditionalPropertiesField field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OAuth2LoginRequest) GetAdditionalPropertiesFieldOk() (map[string]interface{}, bool) { + if o == nil || o.AdditionalPropertiesField == nil { + return nil, false + } + return o.AdditionalPropertiesField, true +} + +// HasAdditionalPropertiesField returns a boolean if a field has been set. +func (o *OAuth2LoginRequest) HasAdditionalPropertiesField() bool { + if o != nil && o.AdditionalPropertiesField != nil { + return true + } + + return false +} + +// SetAdditionalPropertiesField gets a reference to the given map[string]interface{} and assigns it to the AdditionalPropertiesField field. +func (o *OAuth2LoginRequest) SetAdditionalPropertiesField(v map[string]interface{}) { + o.AdditionalPropertiesField = v +} + // GetChallenge returns the Challenge field value if set, zero value otherwise. func (o *OAuth2LoginRequest) GetChallenge() string { if o == nil || o.Challenge == nil { @@ -340,6 +373,9 @@ func (o *OAuth2LoginRequest) SetSubject(v string) { func (o OAuth2LoginRequest) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} + if o.AdditionalPropertiesField != nil { + toSerialize["AdditionalProperties"] = o.AdditionalPropertiesField + } if o.Challenge != nil { toSerialize["challenge"] = o.Challenge } diff --git a/internal/httpclient/model_recovery_flow.go b/internal/httpclient/model_recovery_flow.go index e6df63a7c6b2..56f27a904be1 100644 --- a/internal/httpclient/model_recovery_flow.go +++ b/internal/httpclient/model_recovery_flow.go @@ -34,6 +34,8 @@ type RecoveryFlow struct { ReturnTo *string `json:"return_to,omitempty"` // State represents the state of this request: choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. State interface{} `json:"state"` + // TransientPayload is used to pass data from the recovery flow to hooks and email templates + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -281,6 +283,38 @@ func (o *RecoveryFlow) SetState(v interface{}) { o.State = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *RecoveryFlow) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RecoveryFlow) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *RecoveryFlow) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *RecoveryFlow) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetType returns the Type field value func (o *RecoveryFlow) GetType() string { if o == nil { @@ -355,6 +389,9 @@ func (o RecoveryFlow) MarshalJSON() ([]byte, error) { if o.State != nil { toSerialize["state"] = o.State } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if true { toSerialize["type"] = o.Type } diff --git a/internal/httpclient/model_recovery_flow_state.go b/internal/httpclient/model_recovery_flow_state.go index 1c660ba043b9..d1fa3618882a 100644 --- a/internal/httpclient/model_recovery_flow_state.go +++ b/internal/httpclient/model_recovery_flow_state.go @@ -16,7 +16,7 @@ import ( "fmt" ) -// RecoveryFlowState The state represents the state of the recovery flow. choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. +// RecoveryFlowState The experimental state represents the state of a recovery flow. This field is EXPERIMENTAL and subject to change! type RecoveryFlowState string // List of recoveryFlowState diff --git a/internal/httpclient/model_registration_flow.go b/internal/httpclient/model_registration_flow.go index 71ab2c03ebe2..c0ba64843d3f 100644 --- a/internal/httpclient/model_registration_flow.go +++ b/internal/httpclient/model_registration_flow.go @@ -18,7 +18,7 @@ import ( // RegistrationFlow struct for RegistrationFlow type RegistrationFlow struct { - // Active, if set, contains the registration method that is being used. It is initially not set. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode + // Active, if set, contains the registration method that is being used. It is initially not set. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode Active *string `json:"active,omitempty"` // ExpiresAt is the time (UTC) when the flow expires. If the user still wishes to log in, a new flow has to be initiated. ExpiresAt time.Time `json:"expires_at"` diff --git a/internal/httpclient/model_registration_flow_state.go b/internal/httpclient/model_registration_flow_state.go index 86f3fd38cff0..15fd9f532d4b 100644 --- a/internal/httpclient/model_registration_flow_state.go +++ b/internal/httpclient/model_registration_flow_state.go @@ -16,7 +16,7 @@ import ( "fmt" ) -// RegistrationFlowState choose_method: ask the user to choose a method (e.g. registration with email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the registration challenge was passed. +// RegistrationFlowState The experimental state represents the state of a registration flow. This field is EXPERIMENTAL and subject to change! type RegistrationFlowState string // List of registrationFlowState diff --git a/internal/httpclient/model_settings_flow.go b/internal/httpclient/model_settings_flow.go index fa5cd9317c54..f45c1599e8dd 100644 --- a/internal/httpclient/model_settings_flow.go +++ b/internal/httpclient/model_settings_flow.go @@ -35,6 +35,8 @@ type SettingsFlow struct { ReturnTo *string `json:"return_to,omitempty"` // State represents the state of this flow. It knows two states: show_form: No user data has been collected, or it is invalid, and thus the form should be shown. success: Indicates that the settings flow has been updated successfully with the provided data. Done will stay true when repeatedly checking. If set to true, done will revert back to false only when a flow with invalid (e.g. \"please use a valid phone number\") data was sent. State interface{} `json:"state"` + // TransientPayload is used to pass data from the settings flow to hooks and email templates + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -307,6 +309,38 @@ func (o *SettingsFlow) SetState(v interface{}) { o.State = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *SettingsFlow) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SettingsFlow) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *SettingsFlow) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *SettingsFlow) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetType returns the Type field value func (o *SettingsFlow) GetType() string { if o == nil { @@ -384,6 +418,9 @@ func (o SettingsFlow) MarshalJSON() ([]byte, error) { if o.State != nil { toSerialize["state"] = o.State } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if true { toSerialize["type"] = o.Type } diff --git a/internal/httpclient/model_settings_flow_state.go b/internal/httpclient/model_settings_flow_state.go index f994c786a2d8..70093c9c4a03 100644 --- a/internal/httpclient/model_settings_flow_state.go +++ b/internal/httpclient/model_settings_flow_state.go @@ -16,7 +16,7 @@ import ( "fmt" ) -// SettingsFlowState show_form: No user data has been collected, or it is invalid, and thus the form should be shown. success: Indicates that the settings flow has been updated successfully with the provided data. Done will stay true when repeatedly checking. If set to true, done will revert back to false only when a flow with invalid (e.g. \"please use a valid phone number\") data was sent. +// SettingsFlowState The experimental state represents the state of a settings flow. This field is EXPERIMENTAL and subject to change! type SettingsFlowState string // List of settingsFlowState diff --git a/internal/httpclient/model_successful_native_login.go b/internal/httpclient/model_successful_native_login.go index f87739f19d12..faf59ae906e7 100644 --- a/internal/httpclient/model_successful_native_login.go +++ b/internal/httpclient/model_successful_native_login.go @@ -17,7 +17,9 @@ import ( // SuccessfulNativeLogin The Response for Login Flows via API type SuccessfulNativeLogin struct { - Session Session `json:"session"` + // Contains a list of actions, that could follow this flow It can, for example, this will contain a reference to the verification flow, created as part of the user's registration or the token of the session. + ContinueWith []ContinueWith `json:"continue_with,omitempty"` + Session Session `json:"session"` // The Session Token A session token is equivalent to a session cookie, but it can be sent in the HTTP Authorization Header: Authorization: bearer ${session-token} The session token is only issued for API flows, not for Browser flows! SessionToken *string `json:"session_token,omitempty"` } @@ -40,6 +42,38 @@ func NewSuccessfulNativeLoginWithDefaults() *SuccessfulNativeLogin { return &this } +// GetContinueWith returns the ContinueWith field value if set, zero value otherwise. +func (o *SuccessfulNativeLogin) GetContinueWith() []ContinueWith { + if o == nil || o.ContinueWith == nil { + var ret []ContinueWith + return ret + } + return o.ContinueWith +} + +// GetContinueWithOk returns a tuple with the ContinueWith field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SuccessfulNativeLogin) GetContinueWithOk() ([]ContinueWith, bool) { + if o == nil || o.ContinueWith == nil { + return nil, false + } + return o.ContinueWith, true +} + +// HasContinueWith returns a boolean if a field has been set. +func (o *SuccessfulNativeLogin) HasContinueWith() bool { + if o != nil && o.ContinueWith != nil { + return true + } + + return false +} + +// SetContinueWith gets a reference to the given []ContinueWith and assigns it to the ContinueWith field. +func (o *SuccessfulNativeLogin) SetContinueWith(v []ContinueWith) { + o.ContinueWith = v +} + // GetSession returns the Session field value func (o *SuccessfulNativeLogin) GetSession() Session { if o == nil { @@ -98,6 +132,9 @@ func (o *SuccessfulNativeLogin) SetSessionToken(v string) { func (o SuccessfulNativeLogin) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} + if o.ContinueWith != nil { + toSerialize["continue_with"] = o.ContinueWith + } if true { toSerialize["session"] = o.Session } diff --git a/internal/httpclient/model_ui_node.go b/internal/httpclient/model_ui_node.go index ca88879a7414..3582d9e85f67 100644 --- a/internal/httpclient/model_ui_node.go +++ b/internal/httpclient/model_ui_node.go @@ -18,7 +18,7 @@ import ( // UiNode Nodes are represented as HTML elements or their native UI equivalents. For example, a node can be an `` tag, or an `` but also `some plain text`. type UiNode struct { Attributes UiNodeAttributes `json:"attributes"` - // Group specifies which group (e.g. password authenticator) this node belongs to. default DefaultGroup password PasswordGroup oidc OpenIDConnectGroup profile ProfileGroup link LinkGroup code CodeGroup totp TOTPGroup lookup_secret LookupGroup webauthn WebAuthnGroup + // Group specifies which group (e.g. password authenticator) this node belongs to. default DefaultGroup password PasswordGroup oidc OpenIDConnectGroup profile ProfileGroup link LinkGroup code CodeGroup totp TOTPGroup lookup_secret LookupGroup webauthn WebAuthnGroup passkey PasskeyGroup identifier_first IdentifierFirstGroup Group string `json:"group"` Messages []UiText `json:"messages"` Meta UiNodeMeta `json:"meta"` diff --git a/internal/httpclient/model_ui_node_anchor_attributes.go b/internal/httpclient/model_ui_node_anchor_attributes.go index 15cb492fe8eb..ad2cc992a119 100644 --- a/internal/httpclient/model_ui_node_anchor_attributes.go +++ b/internal/httpclient/model_ui_node_anchor_attributes.go @@ -21,7 +21,7 @@ type UiNodeAnchorAttributes struct { Href string `json:"href"` // A unique identifier Id string `json:"id"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"a\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"a\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` Title UiText `json:"title"` } diff --git a/internal/httpclient/model_ui_node_attributes.go b/internal/httpclient/model_ui_node_attributes.go index 347be6f44a72..510dc20f8564 100644 --- a/internal/httpclient/model_ui_node_attributes.go +++ b/internal/httpclient/model_ui_node_attributes.go @@ -63,86 +63,134 @@ func UiNodeTextAttributesAsUiNodeAttributes(v *UiNodeTextAttributes) UiNodeAttri // Unmarshal JSON data into one of the pointers in the struct func (dst *UiNodeAttributes) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UiNodeAnchorAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeAnchorAttributes) - if err == nil { - jsonUiNodeAnchorAttributes, _ := json.Marshal(dst.UiNodeAnchorAttributes) - if string(jsonUiNodeAnchorAttributes) == "{}" { // empty struct - dst.UiNodeAnchorAttributes = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'a' + if jsonDict["node_type"] == "a" { + // try to unmarshal JSON data into UiNodeAnchorAttributes + err = json.Unmarshal(data, &dst.UiNodeAnchorAttributes) + if err == nil { + return nil // data stored in dst.UiNodeAnchorAttributes, return on the first match } else { - match++ + dst.UiNodeAnchorAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeAnchorAttributes: %s", err.Error()) } - } else { - dst.UiNodeAnchorAttributes = nil } - // try to unmarshal data into UiNodeImageAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeImageAttributes) - if err == nil { - jsonUiNodeImageAttributes, _ := json.Marshal(dst.UiNodeImageAttributes) - if string(jsonUiNodeImageAttributes) == "{}" { // empty struct - dst.UiNodeImageAttributes = nil + // check if the discriminator value is 'img' + if jsonDict["node_type"] == "img" { + // try to unmarshal JSON data into UiNodeImageAttributes + err = json.Unmarshal(data, &dst.UiNodeImageAttributes) + if err == nil { + return nil // data stored in dst.UiNodeImageAttributes, return on the first match } else { - match++ + dst.UiNodeImageAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeImageAttributes: %s", err.Error()) } - } else { - dst.UiNodeImageAttributes = nil } - // try to unmarshal data into UiNodeInputAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeInputAttributes) - if err == nil { - jsonUiNodeInputAttributes, _ := json.Marshal(dst.UiNodeInputAttributes) - if string(jsonUiNodeInputAttributes) == "{}" { // empty struct - dst.UiNodeInputAttributes = nil + // check if the discriminator value is 'input' + if jsonDict["node_type"] == "input" { + // try to unmarshal JSON data into UiNodeInputAttributes + err = json.Unmarshal(data, &dst.UiNodeInputAttributes) + if err == nil { + return nil // data stored in dst.UiNodeInputAttributes, return on the first match } else { - match++ + dst.UiNodeInputAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeInputAttributes: %s", err.Error()) } - } else { - dst.UiNodeInputAttributes = nil } - // try to unmarshal data into UiNodeScriptAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeScriptAttributes) - if err == nil { - jsonUiNodeScriptAttributes, _ := json.Marshal(dst.UiNodeScriptAttributes) - if string(jsonUiNodeScriptAttributes) == "{}" { // empty struct - dst.UiNodeScriptAttributes = nil + // check if the discriminator value is 'script' + if jsonDict["node_type"] == "script" { + // try to unmarshal JSON data into UiNodeScriptAttributes + err = json.Unmarshal(data, &dst.UiNodeScriptAttributes) + if err == nil { + return nil // data stored in dst.UiNodeScriptAttributes, return on the first match } else { - match++ + dst.UiNodeScriptAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeScriptAttributes: %s", err.Error()) } - } else { - dst.UiNodeScriptAttributes = nil } - // try to unmarshal data into UiNodeTextAttributes - err = newStrictDecoder(data).Decode(&dst.UiNodeTextAttributes) - if err == nil { - jsonUiNodeTextAttributes, _ := json.Marshal(dst.UiNodeTextAttributes) - if string(jsonUiNodeTextAttributes) == "{}" { // empty struct + // check if the discriminator value is 'text' + if jsonDict["node_type"] == "text" { + // try to unmarshal JSON data into UiNodeTextAttributes + err = json.Unmarshal(data, &dst.UiNodeTextAttributes) + if err == nil { + return nil // data stored in dst.UiNodeTextAttributes, return on the first match + } else { dst.UiNodeTextAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeTextAttributes: %s", err.Error()) + } + } + + // check if the discriminator value is 'uiNodeAnchorAttributes' + if jsonDict["node_type"] == "uiNodeAnchorAttributes" { + // try to unmarshal JSON data into UiNodeAnchorAttributes + err = json.Unmarshal(data, &dst.UiNodeAnchorAttributes) + if err == nil { + return nil // data stored in dst.UiNodeAnchorAttributes, return on the first match + } else { + dst.UiNodeAnchorAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeAnchorAttributes: %s", err.Error()) + } + } + + // check if the discriminator value is 'uiNodeImageAttributes' + if jsonDict["node_type"] == "uiNodeImageAttributes" { + // try to unmarshal JSON data into UiNodeImageAttributes + err = json.Unmarshal(data, &dst.UiNodeImageAttributes) + if err == nil { + return nil // data stored in dst.UiNodeImageAttributes, return on the first match } else { - match++ + dst.UiNodeImageAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeImageAttributes: %s", err.Error()) } - } else { - dst.UiNodeTextAttributes = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UiNodeAnchorAttributes = nil - dst.UiNodeImageAttributes = nil - dst.UiNodeInputAttributes = nil - dst.UiNodeScriptAttributes = nil - dst.UiNodeTextAttributes = nil + // check if the discriminator value is 'uiNodeInputAttributes' + if jsonDict["node_type"] == "uiNodeInputAttributes" { + // try to unmarshal JSON data into UiNodeInputAttributes + err = json.Unmarshal(data, &dst.UiNodeInputAttributes) + if err == nil { + return nil // data stored in dst.UiNodeInputAttributes, return on the first match + } else { + dst.UiNodeInputAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeInputAttributes: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UiNodeAttributes)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UiNodeAttributes)") + // check if the discriminator value is 'uiNodeScriptAttributes' + if jsonDict["node_type"] == "uiNodeScriptAttributes" { + // try to unmarshal JSON data into UiNodeScriptAttributes + err = json.Unmarshal(data, &dst.UiNodeScriptAttributes) + if err == nil { + return nil // data stored in dst.UiNodeScriptAttributes, return on the first match + } else { + dst.UiNodeScriptAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeScriptAttributes: %s", err.Error()) + } + } + + // check if the discriminator value is 'uiNodeTextAttributes' + if jsonDict["node_type"] == "uiNodeTextAttributes" { + // try to unmarshal JSON data into UiNodeTextAttributes + err = json.Unmarshal(data, &dst.UiNodeTextAttributes) + if err == nil { + return nil // data stored in dst.UiNodeTextAttributes, return on the first match + } else { + dst.UiNodeTextAttributes = nil + return fmt.Errorf("Failed to unmarshal UiNodeAttributes as UiNodeTextAttributes: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/httpclient/model_ui_node_image_attributes.go b/internal/httpclient/model_ui_node_image_attributes.go index 5b300b9548bd..6eef160e3d67 100644 --- a/internal/httpclient/model_ui_node_image_attributes.go +++ b/internal/httpclient/model_ui_node_image_attributes.go @@ -21,7 +21,7 @@ type UiNodeImageAttributes struct { Height int64 `json:"height"` // A unique identifier Id string `json:"id"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"img\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"img\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` // The image's source URL. format: uri Src string `json:"src"` diff --git a/internal/httpclient/model_ui_node_input_attributes.go b/internal/httpclient/model_ui_node_input_attributes.go index 3ed9dd56e0c6..2c7ce0e2e5a9 100644 --- a/internal/httpclient/model_ui_node_input_attributes.go +++ b/internal/httpclient/model_ui_node_input_attributes.go @@ -22,16 +22,22 @@ type UiNodeInputAttributes struct { // Sets the input's disabled field to true or false. Disabled bool `json:"disabled"` Label *UiText `json:"label,omitempty"` - // The maxlength attribute for the input. - Maxlength *int32 `json:"maxlength,omitempty"` + // MaxLength may contain the input's maximum length. + Maxlength *int64 `json:"maxlength,omitempty"` // The minlength attribute for the input. Minlength *int32 `json:"minlength,omitempty"` // The input's element name. Name string `json:"name"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` - // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. + // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. Deprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead. Onclick *string `json:"onclick,omitempty"` + // OnClickTrigger may contain a WebAuthn trigger which should be executed on click. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration + OnclickTrigger *string `json:"onclickTrigger,omitempty"` + // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn. Deprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead. + Onload *string `json:"onload,omitempty"` + // OnLoadTrigger may contain a WebAuthn trigger which should be executed on load. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration + OnloadTrigger *string `json:"onloadTrigger,omitempty"` // The input's pattern. Pattern *string `json:"pattern,omitempty"` // Mark this input field as required. @@ -152,9 +158,9 @@ func (o *UiNodeInputAttributes) SetLabel(v UiText) { } // GetMaxlength returns the Maxlength field value if set, zero value otherwise. -func (o *UiNodeInputAttributes) GetMaxlength() int32 { +func (o *UiNodeInputAttributes) GetMaxlength() int64 { if o == nil || o.Maxlength == nil { - var ret int32 + var ret int64 return ret } return *o.Maxlength @@ -162,7 +168,7 @@ func (o *UiNodeInputAttributes) GetMaxlength() int32 { // GetMaxlengthOk returns a tuple with the Maxlength field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *UiNodeInputAttributes) GetMaxlengthOk() (*int32, bool) { +func (o *UiNodeInputAttributes) GetMaxlengthOk() (*int64, bool) { if o == nil || o.Maxlength == nil { return nil, false } @@ -178,8 +184,8 @@ func (o *UiNodeInputAttributes) HasMaxlength() bool { return false } -// SetMaxlength gets a reference to the given int32 and assigns it to the Maxlength field. -func (o *UiNodeInputAttributes) SetMaxlength(v int32) { +// SetMaxlength gets a reference to the given int64 and assigns it to the Maxlength field. +func (o *UiNodeInputAttributes) SetMaxlength(v int64) { o.Maxlength = &v } @@ -295,6 +301,102 @@ func (o *UiNodeInputAttributes) SetOnclick(v string) { o.Onclick = &v } +// GetOnclickTrigger returns the OnclickTrigger field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOnclickTrigger() string { + if o == nil || o.OnclickTrigger == nil { + var ret string + return ret + } + return *o.OnclickTrigger +} + +// GetOnclickTriggerOk returns a tuple with the OnclickTrigger field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOnclickTriggerOk() (*string, bool) { + if o == nil || o.OnclickTrigger == nil { + return nil, false + } + return o.OnclickTrigger, true +} + +// HasOnclickTrigger returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOnclickTrigger() bool { + if o != nil && o.OnclickTrigger != nil { + return true + } + + return false +} + +// SetOnclickTrigger gets a reference to the given string and assigns it to the OnclickTrigger field. +func (o *UiNodeInputAttributes) SetOnclickTrigger(v string) { + o.OnclickTrigger = &v +} + +// GetOnload returns the Onload field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOnload() string { + if o == nil || o.Onload == nil { + var ret string + return ret + } + return *o.Onload +} + +// GetOnloadOk returns a tuple with the Onload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOnloadOk() (*string, bool) { + if o == nil || o.Onload == nil { + return nil, false + } + return o.Onload, true +} + +// HasOnload returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOnload() bool { + if o != nil && o.Onload != nil { + return true + } + + return false +} + +// SetOnload gets a reference to the given string and assigns it to the Onload field. +func (o *UiNodeInputAttributes) SetOnload(v string) { + o.Onload = &v +} + +// GetOnloadTrigger returns the OnloadTrigger field value if set, zero value otherwise. +func (o *UiNodeInputAttributes) GetOnloadTrigger() string { + if o == nil || o.OnloadTrigger == nil { + var ret string + return ret + } + return *o.OnloadTrigger +} + +// GetOnloadTriggerOk returns a tuple with the OnloadTrigger field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UiNodeInputAttributes) GetOnloadTriggerOk() (*string, bool) { + if o == nil || o.OnloadTrigger == nil { + return nil, false + } + return o.OnloadTrigger, true +} + +// HasOnloadTrigger returns a boolean if a field has been set. +func (o *UiNodeInputAttributes) HasOnloadTrigger() bool { + if o != nil && o.OnloadTrigger != nil { + return true + } + + return false +} + +// SetOnloadTrigger gets a reference to the given string and assigns it to the OnloadTrigger field. +func (o *UiNodeInputAttributes) SetOnloadTrigger(v string) { + o.OnloadTrigger = &v +} + // GetPattern returns the Pattern field value if set, zero value otherwise. func (o *UiNodeInputAttributes) GetPattern() string { if o == nil || o.Pattern == nil { @@ -442,6 +544,15 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) { if o.Onclick != nil { toSerialize["onclick"] = o.Onclick } + if o.OnclickTrigger != nil { + toSerialize["onclickTrigger"] = o.OnclickTrigger + } + if o.Onload != nil { + toSerialize["onload"] = o.Onload + } + if o.OnloadTrigger != nil { + toSerialize["onloadTrigger"] = o.OnloadTrigger + } if o.Pattern != nil { toSerialize["pattern"] = o.Pattern } diff --git a/internal/httpclient/model_ui_node_script_attributes.go b/internal/httpclient/model_ui_node_script_attributes.go index 21dca70cbe86..d867c1c66dba 100644 --- a/internal/httpclient/model_ui_node_script_attributes.go +++ b/internal/httpclient/model_ui_node_script_attributes.go @@ -25,7 +25,7 @@ type UiNodeScriptAttributes struct { Id string `json:"id"` // The script's integrity hash Integrity string `json:"integrity"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"script\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"script\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` // Nonce for CSP A nonce you may want to use to improve your Content Security Policy. You do not have to use this value but if you want to improve your CSP policies you may use it. You can also choose to use your own nonce value! Nonce string `json:"nonce"` diff --git a/internal/httpclient/model_ui_node_text_attributes.go b/internal/httpclient/model_ui_node_text_attributes.go index 6199187b5d75..93e0f8314191 100644 --- a/internal/httpclient/model_ui_node_text_attributes.go +++ b/internal/httpclient/model_ui_node_text_attributes.go @@ -19,7 +19,7 @@ import ( type UiNodeTextAttributes struct { // A unique identifier Id string `json:"id"` - // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"text\". + // NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"text\". text Text input Input img Image a Anchor script Script NodeType string `json:"node_type"` Text UiText `json:"text"` } diff --git a/internal/httpclient/model_update_login_flow_body.go b/internal/httpclient/model_update_login_flow_body.go index 36033328e78d..f0d79322c54f 100644 --- a/internal/httpclient/model_update_login_flow_body.go +++ b/internal/httpclient/model_update_login_flow_body.go @@ -18,12 +18,14 @@ import ( // UpdateLoginFlowBody - struct for UpdateLoginFlowBody type UpdateLoginFlowBody struct { - UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod - UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod - UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod - UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod - UpdateLoginFlowWithTotpMethod *UpdateLoginFlowWithTotpMethod - UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod + UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod + UpdateLoginFlowWithIdentifierFirstMethod *UpdateLoginFlowWithIdentifierFirstMethod + UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod + UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod + UpdateLoginFlowWithPasskeyMethod *UpdateLoginFlowWithPasskeyMethod + UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod + UpdateLoginFlowWithTotpMethod *UpdateLoginFlowWithTotpMethod + UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod } // UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithCodeMethod wrapped in UpdateLoginFlowBody @@ -33,6 +35,13 @@ func UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithCo } } +// UpdateLoginFlowWithIdentifierFirstMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithIdentifierFirstMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithIdentifierFirstMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithIdentifierFirstMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithIdentifierFirstMethod: v, + } +} + // UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithLookupSecretMethod wrapped in UpdateLoginFlowBody func UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithLookupSecretMethod) UpdateLoginFlowBody { return UpdateLoginFlowBody{ @@ -47,6 +56,13 @@ func UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithOi } } +// UpdateLoginFlowWithPasskeyMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithPasskeyMethod wrapped in UpdateLoginFlowBody +func UpdateLoginFlowWithPasskeyMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithPasskeyMethod) UpdateLoginFlowBody { + return UpdateLoginFlowBody{ + UpdateLoginFlowWithPasskeyMethod: v, + } +} + // UpdateLoginFlowWithPasswordMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithPasswordMethod wrapped in UpdateLoginFlowBody func UpdateLoginFlowWithPasswordMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithPasswordMethod) UpdateLoginFlowBody { return UpdateLoginFlowBody{ @@ -71,100 +87,206 @@ func UpdateLoginFlowWithWebAuthnMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWi // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateLoginFlowWithCodeMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithCodeMethod) - if err == nil { - jsonUpdateLoginFlowWithCodeMethod, _ := json.Marshal(dst.UpdateLoginFlowWithCodeMethod) - if string(jsonUpdateLoginFlowWithCodeMethod) == "{}" { // empty struct + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'code' + if jsonDict["method"] == "code" { + // try to unmarshal JSON data into UpdateLoginFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithCodeMethod, return on the first match + } else { dst.UpdateLoginFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithCodeMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'identifier_first' + if jsonDict["method"] == "identifier_first" { + // try to unmarshal JSON data into UpdateLoginFlowWithIdentifierFirstMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithIdentifierFirstMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithIdentifierFirstMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithIdentifierFirstMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithIdentifierFirstMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithCodeMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithLookupSecretMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithLookupSecretMethod) - if err == nil { - jsonUpdateLoginFlowWithLookupSecretMethod, _ := json.Marshal(dst.UpdateLoginFlowWithLookupSecretMethod) - if string(jsonUpdateLoginFlowWithLookupSecretMethod) == "{}" { // empty struct - dst.UpdateLoginFlowWithLookupSecretMethod = nil + // check if the discriminator value is 'lookup_secret' + if jsonDict["method"] == "lookup_secret" { + // try to unmarshal JSON data into UpdateLoginFlowWithLookupSecretMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithLookupSecretMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithLookupSecretMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithLookupSecretMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithLookupSecretMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithLookupSecretMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithOidcMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithOidcMethod) - if err == nil { - jsonUpdateLoginFlowWithOidcMethod, _ := json.Marshal(dst.UpdateLoginFlowWithOidcMethod) - if string(jsonUpdateLoginFlowWithOidcMethod) == "{}" { // empty struct + // check if the discriminator value is 'oidc' + if jsonDict["method"] == "oidc" { + // try to unmarshal JSON data into UpdateLoginFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithOidcMethod, return on the first match + } else { dst.UpdateLoginFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithOidcMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'passkey' + if jsonDict["method"] == "passkey" { + // try to unmarshal JSON data into UpdateLoginFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithPasskeyMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasskeyMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithOidcMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithPasswordMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithPasswordMethod) - if err == nil { - jsonUpdateLoginFlowWithPasswordMethod, _ := json.Marshal(dst.UpdateLoginFlowWithPasswordMethod) - if string(jsonUpdateLoginFlowWithPasswordMethod) == "{}" { // empty struct - dst.UpdateLoginFlowWithPasswordMethod = nil + // check if the discriminator value is 'password' + if jsonDict["method"] == "password" { + // try to unmarshal JSON data into UpdateLoginFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithPasswordMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasswordMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithPasswordMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithTotpMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithTotpMethod) - if err == nil { - jsonUpdateLoginFlowWithTotpMethod, _ := json.Marshal(dst.UpdateLoginFlowWithTotpMethod) - if string(jsonUpdateLoginFlowWithTotpMethod) == "{}" { // empty struct - dst.UpdateLoginFlowWithTotpMethod = nil + // check if the discriminator value is 'totp' + if jsonDict["method"] == "totp" { + // try to unmarshal JSON data into UpdateLoginFlowWithTotpMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithTotpMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithTotpMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithTotpMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithTotpMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithTotpMethod = nil } - // try to unmarshal data into UpdateLoginFlowWithWebAuthnMethod - err = newStrictDecoder(data).Decode(&dst.UpdateLoginFlowWithWebAuthnMethod) - if err == nil { - jsonUpdateLoginFlowWithWebAuthnMethod, _ := json.Marshal(dst.UpdateLoginFlowWithWebAuthnMethod) - if string(jsonUpdateLoginFlowWithWebAuthnMethod) == "{}" { // empty struct + // check if the discriminator value is 'webauthn' + if jsonDict["method"] == "webauthn" { + // try to unmarshal JSON data into UpdateLoginFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithWebAuthnMethod, return on the first match + } else { dst.UpdateLoginFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithWebAuthnMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateLoginFlowWithCodeMethod' + if jsonDict["method"] == "updateLoginFlowWithCodeMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateLoginFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateLoginFlowWithWebAuthnMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateLoginFlowWithCodeMethod = nil - dst.UpdateLoginFlowWithLookupSecretMethod = nil - dst.UpdateLoginFlowWithOidcMethod = nil - dst.UpdateLoginFlowWithPasswordMethod = nil - dst.UpdateLoginFlowWithTotpMethod = nil - dst.UpdateLoginFlowWithWebAuthnMethod = nil + // check if the discriminator value is 'updateLoginFlowWithIdentifierFirstMethod' + if jsonDict["method"] == "updateLoginFlowWithIdentifierFirstMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithIdentifierFirstMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithIdentifierFirstMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithIdentifierFirstMethod, return on the first match + } else { + dst.UpdateLoginFlowWithIdentifierFirstMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithIdentifierFirstMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateLoginFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateLoginFlowBody)") + // check if the discriminator value is 'updateLoginFlowWithLookupSecretMethod' + if jsonDict["method"] == "updateLoginFlowWithLookupSecretMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithLookupSecretMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithLookupSecretMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithLookupSecretMethod, return on the first match + } else { + dst.UpdateLoginFlowWithLookupSecretMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithLookupSecretMethod: %s", err.Error()) + } } + + // check if the discriminator value is 'updateLoginFlowWithOidcMethod' + if jsonDict["method"] == "updateLoginFlowWithOidcMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithOidcMethod, return on the first match + } else { + dst.UpdateLoginFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithOidcMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateLoginFlowWithPasskeyMethod' + if jsonDict["method"] == "updateLoginFlowWithPasskeyMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithPasskeyMethod, return on the first match + } else { + dst.UpdateLoginFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasskeyMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateLoginFlowWithPasswordMethod' + if jsonDict["method"] == "updateLoginFlowWithPasswordMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithPasswordMethod, return on the first match + } else { + dst.UpdateLoginFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithPasswordMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateLoginFlowWithTotpMethod' + if jsonDict["method"] == "updateLoginFlowWithTotpMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithTotpMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithTotpMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithTotpMethod, return on the first match + } else { + dst.UpdateLoginFlowWithTotpMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithTotpMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateLoginFlowWithWebAuthnMethod' + if jsonDict["method"] == "updateLoginFlowWithWebAuthnMethod" { + // try to unmarshal JSON data into UpdateLoginFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateLoginFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateLoginFlowWithWebAuthnMethod, return on the first match + } else { + dst.UpdateLoginFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithWebAuthnMethod: %s", err.Error()) + } + } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON @@ -173,6 +295,10 @@ func (src UpdateLoginFlowBody) MarshalJSON() ([]byte, error) { return json.Marshal(&src.UpdateLoginFlowWithCodeMethod) } + if src.UpdateLoginFlowWithIdentifierFirstMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithIdentifierFirstMethod) + } + if src.UpdateLoginFlowWithLookupSecretMethod != nil { return json.Marshal(&src.UpdateLoginFlowWithLookupSecretMethod) } @@ -181,6 +307,10 @@ func (src UpdateLoginFlowBody) MarshalJSON() ([]byte, error) { return json.Marshal(&src.UpdateLoginFlowWithOidcMethod) } + if src.UpdateLoginFlowWithPasskeyMethod != nil { + return json.Marshal(&src.UpdateLoginFlowWithPasskeyMethod) + } + if src.UpdateLoginFlowWithPasswordMethod != nil { return json.Marshal(&src.UpdateLoginFlowWithPasswordMethod) } @@ -205,6 +335,10 @@ func (obj *UpdateLoginFlowBody) GetActualInstance() interface{} { return obj.UpdateLoginFlowWithCodeMethod } + if obj.UpdateLoginFlowWithIdentifierFirstMethod != nil { + return obj.UpdateLoginFlowWithIdentifierFirstMethod + } + if obj.UpdateLoginFlowWithLookupSecretMethod != nil { return obj.UpdateLoginFlowWithLookupSecretMethod } @@ -213,6 +347,10 @@ func (obj *UpdateLoginFlowBody) GetActualInstance() interface{} { return obj.UpdateLoginFlowWithOidcMethod } + if obj.UpdateLoginFlowWithPasskeyMethod != nil { + return obj.UpdateLoginFlowWithPasskeyMethod + } + if obj.UpdateLoginFlowWithPasswordMethod != nil { return obj.UpdateLoginFlowWithPasswordMethod } diff --git a/internal/httpclient/model_update_login_flow_with_code_method.go b/internal/httpclient/model_update_login_flow_with_code_method.go index bd97ab583ebc..06272618da90 100644 --- a/internal/httpclient/model_update_login_flow_with_code_method.go +++ b/internal/httpclient/model_update_login_flow_with_code_method.go @@ -17,6 +17,8 @@ import ( // UpdateLoginFlowWithCodeMethod Update Login flow using the code method type UpdateLoginFlowWithCodeMethod struct { + // Address is the address to send the code to, in case that there are multiple addresses. This field is only used in two-factor flows and is ineffective for passwordless flows. + Address *string `json:"address,omitempty"` // Code is the 6 digits code sent to the user Code *string `json:"code,omitempty"` // CSRFToken is the anti-CSRF token @@ -27,6 +29,8 @@ type UpdateLoginFlowWithCodeMethod struct { Method string `json:"method"` // Resend is set when the user wants to resend the code Resend *string `json:"resend,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateLoginFlowWithCodeMethod instantiates a new UpdateLoginFlowWithCodeMethod object @@ -48,6 +52,38 @@ func NewUpdateLoginFlowWithCodeMethodWithDefaults() *UpdateLoginFlowWithCodeMeth return &this } +// GetAddress returns the Address field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetAddress() string { + if o == nil || o.Address == nil { + var ret string + return ret + } + return *o.Address +} + +// GetAddressOk returns a tuple with the Address field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetAddressOk() (*string, bool) { + if o == nil || o.Address == nil { + return nil, false + } + return o.Address, true +} + +// HasAddress returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasAddress() bool { + if o != nil && o.Address != nil { + return true + } + + return false +} + +// SetAddress gets a reference to the given string and assigns it to the Address field. +func (o *UpdateLoginFlowWithCodeMethod) SetAddress(v string) { + o.Address = &v +} + // GetCode returns the Code field value if set, zero value otherwise. func (o *UpdateLoginFlowWithCodeMethod) GetCode() string { if o == nil || o.Code == nil { @@ -192,8 +228,43 @@ func (o *UpdateLoginFlowWithCodeMethod) SetResend(v string) { o.Resend = &v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithCodeMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} + if o.Address != nil { + toSerialize["address"] = o.Address + } if o.Code != nil { toSerialize["code"] = o.Code } @@ -209,6 +280,9 @@ func (o UpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { if o.Resend != nil { toSerialize["resend"] = o.Resend } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_login_flow_with_identifier_first_method.go b/internal/httpclient/model_update_login_flow_with_identifier_first_method.go new file mode 100644 index 000000000000..70cf8002990d --- /dev/null +++ b/internal/httpclient/model_update_login_flow_with_identifier_first_method.go @@ -0,0 +1,212 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateLoginFlowWithIdentifierFirstMethod Update Login Flow with Multi-Step Method +type UpdateLoginFlowWithIdentifierFirstMethod struct { + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // Identifier is the email or username of the user trying to log in. + Identifier string `json:"identifier"` + // Method should be set to \"password\" when logging in using the identifier and password strategy. + Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` +} + +// NewUpdateLoginFlowWithIdentifierFirstMethod instantiates a new UpdateLoginFlowWithIdentifierFirstMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateLoginFlowWithIdentifierFirstMethod(identifier string, method string) *UpdateLoginFlowWithIdentifierFirstMethod { + this := UpdateLoginFlowWithIdentifierFirstMethod{} + this.Identifier = identifier + this.Method = method + return &this +} + +// NewUpdateLoginFlowWithIdentifierFirstMethodWithDefaults instantiates a new UpdateLoginFlowWithIdentifierFirstMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateLoginFlowWithIdentifierFirstMethodWithDefaults() *UpdateLoginFlowWithIdentifierFirstMethod { + this := UpdateLoginFlowWithIdentifierFirstMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetIdentifier returns the Identifier field value +func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetIdentifier() string { + if o == nil { + var ret string + return ret + } + + return o.Identifier +} + +// GetIdentifierOk returns a tuple with the Identifier field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetIdentifierOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Identifier, true +} + +// SetIdentifier sets field value +func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetIdentifier(v string) { + o.Identifier = v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetMethod(v string) { + o.Method = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateLoginFlowWithIdentifierFirstMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["identifier"] = o.Identifier + } + if true { + toSerialize["method"] = o.Method + } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } + return json.Marshal(toSerialize) +} + +type NullableUpdateLoginFlowWithIdentifierFirstMethod struct { + value *UpdateLoginFlowWithIdentifierFirstMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithIdentifierFirstMethod) Get() *UpdateLoginFlowWithIdentifierFirstMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithIdentifierFirstMethod) Set(val *UpdateLoginFlowWithIdentifierFirstMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithIdentifierFirstMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithIdentifierFirstMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithIdentifierFirstMethod(val *UpdateLoginFlowWithIdentifierFirstMethod) *NullableUpdateLoginFlowWithIdentifierFirstMethod { + return &NullableUpdateLoginFlowWithIdentifierFirstMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithIdentifierFirstMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithIdentifierFirstMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_update_login_flow_with_oidc_method.go b/internal/httpclient/model_update_login_flow_with_oidc_method.go index c196eb2121da..cdd5c665bdc5 100644 --- a/internal/httpclient/model_update_login_flow_with_oidc_method.go +++ b/internal/httpclient/model_update_login_flow_with_oidc_method.go @@ -19,7 +19,7 @@ import ( type UpdateLoginFlowWithOidcMethod struct { // The CSRF Token CsrfToken *string `json:"csrf_token,omitempty"` - // IDToken is an optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple + // IDToken is an optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple Google IdToken *string `json:"id_token,omitempty"` // IDTokenNonce is the nonce, used when generating the IDToken. If the provider supports nonce validation, the nonce will be validated against this value and required. IdTokenNonce *string `json:"id_token_nonce,omitempty"` @@ -29,6 +29,8 @@ type UpdateLoginFlowWithOidcMethod struct { Provider string `json:"provider"` // The identity traits. This is a placeholder for the registration flow. Traits map[string]interface{} `json:"traits,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // UpstreamParameters are the parameters that are passed to the upstream identity provider. These parameters are optional and depend on what the upstream identity provider supports. Supported parameters are: `login_hint` (string): The `login_hint` parameter suppresses the account chooser and either pre-fills the email box on the sign-in form, or selects the proper session. `hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`. `prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`. UpstreamParameters map[string]interface{} `json:"upstream_parameters,omitempty"` } @@ -228,6 +230,38 @@ func (o *UpdateLoginFlowWithOidcMethod) SetTraits(v map[string]interface{}) { o.Traits = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithOidcMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithOidcMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetUpstreamParameters returns the UpstreamParameters field value if set, zero value otherwise. func (o *UpdateLoginFlowWithOidcMethod) GetUpstreamParameters() map[string]interface{} { if o == nil || o.UpstreamParameters == nil { @@ -280,6 +314,9 @@ func (o UpdateLoginFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if o.Traits != nil { toSerialize["traits"] = o.Traits } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if o.UpstreamParameters != nil { toSerialize["upstream_parameters"] = o.UpstreamParameters } diff --git a/internal/httpclient/model_update_login_flow_with_passkey_method.go b/internal/httpclient/model_update_login_flow_with_passkey_method.go new file mode 100644 index 000000000000..90bbcd6ddf1c --- /dev/null +++ b/internal/httpclient/model_update_login_flow_with_passkey_method.go @@ -0,0 +1,182 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateLoginFlowWithPasskeyMethod Update Login Flow with Passkey Method +type UpdateLoginFlowWithPasskeyMethod struct { + // Sending the anti-csrf token is only required for browser login flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // Method should be set to \"passkey\" when logging in using the Passkey strategy. + Method string `json:"method"` + // Login a WebAuthn Security Key This must contain the ID of the WebAuthN connection. + PasskeyLogin *string `json:"passkey_login,omitempty"` +} + +// NewUpdateLoginFlowWithPasskeyMethod instantiates a new UpdateLoginFlowWithPasskeyMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateLoginFlowWithPasskeyMethod(method string) *UpdateLoginFlowWithPasskeyMethod { + this := UpdateLoginFlowWithPasskeyMethod{} + this.Method = method + return &this +} + +// NewUpdateLoginFlowWithPasskeyMethodWithDefaults instantiates a new UpdateLoginFlowWithPasskeyMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateLoginFlowWithPasskeyMethodWithDefaults() *UpdateLoginFlowWithPasskeyMethod { + this := UpdateLoginFlowWithPasskeyMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithPasskeyMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateLoginFlowWithPasskeyMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateLoginFlowWithPasskeyMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateLoginFlowWithPasskeyMethod) SetMethod(v string) { + o.Method = v +} + +// GetPasskeyLogin returns the PasskeyLogin field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithPasskeyMethod) GetPasskeyLogin() string { + if o == nil || o.PasskeyLogin == nil { + var ret string + return ret + } + return *o.PasskeyLogin +} + +// GetPasskeyLoginOk returns a tuple with the PasskeyLogin field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) GetPasskeyLoginOk() (*string, bool) { + if o == nil || o.PasskeyLogin == nil { + return nil, false + } + return o.PasskeyLogin, true +} + +// HasPasskeyLogin returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithPasskeyMethod) HasPasskeyLogin() bool { + if o != nil && o.PasskeyLogin != nil { + return true + } + + return false +} + +// SetPasskeyLogin gets a reference to the given string and assigns it to the PasskeyLogin field. +func (o *UpdateLoginFlowWithPasskeyMethod) SetPasskeyLogin(v string) { + o.PasskeyLogin = &v +} + +func (o UpdateLoginFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.PasskeyLogin != nil { + toSerialize["passkey_login"] = o.PasskeyLogin + } + return json.Marshal(toSerialize) +} + +type NullableUpdateLoginFlowWithPasskeyMethod struct { + value *UpdateLoginFlowWithPasskeyMethod + isSet bool +} + +func (v NullableUpdateLoginFlowWithPasskeyMethod) Get() *UpdateLoginFlowWithPasskeyMethod { + return v.value +} + +func (v *NullableUpdateLoginFlowWithPasskeyMethod) Set(val *UpdateLoginFlowWithPasskeyMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateLoginFlowWithPasskeyMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateLoginFlowWithPasskeyMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateLoginFlowWithPasskeyMethod(val *UpdateLoginFlowWithPasskeyMethod) *NullableUpdateLoginFlowWithPasskeyMethod { + return &NullableUpdateLoginFlowWithPasskeyMethod{value: val, isSet: true} +} + +func (v NullableUpdateLoginFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateLoginFlowWithPasskeyMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_update_login_flow_with_password_method.go b/internal/httpclient/model_update_login_flow_with_password_method.go index 35a9b590bd04..4bad1a416326 100644 --- a/internal/httpclient/model_update_login_flow_with_password_method.go +++ b/internal/httpclient/model_update_login_flow_with_password_method.go @@ -27,6 +27,8 @@ type UpdateLoginFlowWithPasswordMethod struct { Password string `json:"password"` // Identifier is the email or username of the user trying to log in. This field is deprecated! PasswordIdentifier *string `json:"password_identifier,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateLoginFlowWithPasswordMethod instantiates a new UpdateLoginFlowWithPasswordMethod object @@ -185,6 +187,38 @@ func (o *UpdateLoginFlowWithPasswordMethod) SetPasswordIdentifier(v string) { o.PasswordIdentifier = &v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithPasswordMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithPasswordMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithPasswordMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithPasswordMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateLoginFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -202,6 +236,9 @@ func (o UpdateLoginFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { if o.PasswordIdentifier != nil { toSerialize["password_identifier"] = o.PasswordIdentifier } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_login_flow_with_totp_method.go b/internal/httpclient/model_update_login_flow_with_totp_method.go index 34d3b316dd01..32a94efb20f4 100644 --- a/internal/httpclient/model_update_login_flow_with_totp_method.go +++ b/internal/httpclient/model_update_login_flow_with_totp_method.go @@ -23,6 +23,8 @@ type UpdateLoginFlowWithTotpMethod struct { Method string `json:"method"` // The TOTP code. TotpCode string `json:"totp_code"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateLoginFlowWithTotpMethod instantiates a new UpdateLoginFlowWithTotpMethod object @@ -124,6 +126,38 @@ func (o *UpdateLoginFlowWithTotpMethod) SetTotpCode(v string) { o.TotpCode = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithTotpMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithTotpMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithTotpMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithTotpMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateLoginFlowWithTotpMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateLoginFlowWithTotpMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["totp_code"] = o.TotpCode } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_login_flow_with_web_authn_method.go b/internal/httpclient/model_update_login_flow_with_web_authn_method.go index d5d8554febb4..1c3211a510ed 100644 --- a/internal/httpclient/model_update_login_flow_with_web_authn_method.go +++ b/internal/httpclient/model_update_login_flow_with_web_authn_method.go @@ -23,6 +23,8 @@ type UpdateLoginFlowWithWebAuthnMethod struct { Identifier string `json:"identifier"` // Method should be set to \"webAuthn\" when logging in using the WebAuthn strategy. Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // Login a WebAuthn Security Key This must contain the ID of the WebAuthN connection. WebauthnLogin *string `json:"webauthn_login,omitempty"` } @@ -126,6 +128,38 @@ func (o *UpdateLoginFlowWithWebAuthnMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithWebAuthnMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithWebAuthnMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithWebAuthnMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateLoginFlowWithWebAuthnMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetWebauthnLogin returns the WebauthnLogin field value if set, zero value otherwise. func (o *UpdateLoginFlowWithWebAuthnMethod) GetWebauthnLogin() string { if o == nil || o.WebauthnLogin == nil { @@ -169,6 +203,9 @@ func (o UpdateLoginFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if o.WebauthnLogin != nil { toSerialize["webauthn_login"] = o.WebauthnLogin } diff --git a/internal/httpclient/model_update_recovery_flow_body.go b/internal/httpclient/model_update_recovery_flow_body.go index 6eea1d5d6b6a..b0f6de861b4f 100644 --- a/internal/httpclient/model_update_recovery_flow_body.go +++ b/internal/httpclient/model_update_recovery_flow_body.go @@ -39,44 +39,62 @@ func UpdateRecoveryFlowWithLinkMethodAsUpdateRecoveryFlowBody(v *UpdateRecoveryF // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateRecoveryFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateRecoveryFlowWithCodeMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRecoveryFlowWithCodeMethod) - if err == nil { - jsonUpdateRecoveryFlowWithCodeMethod, _ := json.Marshal(dst.UpdateRecoveryFlowWithCodeMethod) - if string(jsonUpdateRecoveryFlowWithCodeMethod) == "{}" { // empty struct - dst.UpdateRecoveryFlowWithCodeMethod = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'code' + if jsonDict["method"] == "code" { + // try to unmarshal JSON data into UpdateRecoveryFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateRecoveryFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateRecoveryFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateRecoveryFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateRecoveryFlowWithCodeMethod = nil } - // try to unmarshal data into UpdateRecoveryFlowWithLinkMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRecoveryFlowWithLinkMethod) - if err == nil { - jsonUpdateRecoveryFlowWithLinkMethod, _ := json.Marshal(dst.UpdateRecoveryFlowWithLinkMethod) - if string(jsonUpdateRecoveryFlowWithLinkMethod) == "{}" { // empty struct - dst.UpdateRecoveryFlowWithLinkMethod = nil + // check if the discriminator value is 'link' + if jsonDict["method"] == "link" { + // try to unmarshal JSON data into UpdateRecoveryFlowWithLinkMethod + err = json.Unmarshal(data, &dst.UpdateRecoveryFlowWithLinkMethod) + if err == nil { + return nil // data stored in dst.UpdateRecoveryFlowWithLinkMethod, return on the first match } else { - match++ + dst.UpdateRecoveryFlowWithLinkMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithLinkMethod: %s", err.Error()) } - } else { - dst.UpdateRecoveryFlowWithLinkMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateRecoveryFlowWithCodeMethod = nil - dst.UpdateRecoveryFlowWithLinkMethod = nil + // check if the discriminator value is 'updateRecoveryFlowWithCodeMethod' + if jsonDict["method"] == "updateRecoveryFlowWithCodeMethod" { + // try to unmarshal JSON data into UpdateRecoveryFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateRecoveryFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateRecoveryFlowWithCodeMethod, return on the first match + } else { + dst.UpdateRecoveryFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithCodeMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateRecoveryFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateRecoveryFlowBody)") + // check if the discriminator value is 'updateRecoveryFlowWithLinkMethod' + if jsonDict["method"] == "updateRecoveryFlowWithLinkMethod" { + // try to unmarshal JSON data into UpdateRecoveryFlowWithLinkMethod + err = json.Unmarshal(data, &dst.UpdateRecoveryFlowWithLinkMethod) + if err == nil { + return nil // data stored in dst.UpdateRecoveryFlowWithLinkMethod, return on the first match + } else { + dst.UpdateRecoveryFlowWithLinkMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRecoveryFlowBody as UpdateRecoveryFlowWithLinkMethod: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/httpclient/model_update_recovery_flow_with_code_method.go b/internal/httpclient/model_update_recovery_flow_with_code_method.go index 8d440330c7b8..50aad2ca2945 100644 --- a/internal/httpclient/model_update_recovery_flow_with_code_method.go +++ b/internal/httpclient/model_update_recovery_flow_with_code_method.go @@ -25,6 +25,8 @@ type UpdateRecoveryFlowWithCodeMethod struct { Email *string `json:"email,omitempty"` // Method is the method that should be used for this recovery flow Allowed values are `link` and `code`. link RecoveryStrategyLink code RecoveryStrategyCode Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateRecoveryFlowWithCodeMethod instantiates a new UpdateRecoveryFlowWithCodeMethod object @@ -165,6 +167,38 @@ func (o *UpdateRecoveryFlowWithCodeMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRecoveryFlowWithCodeMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateRecoveryFlowWithCodeMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.Code != nil { @@ -179,6 +213,9 @@ func (o UpdateRecoveryFlowWithCodeMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_recovery_flow_with_link_method.go b/internal/httpclient/model_update_recovery_flow_with_link_method.go index 8a8861d86f07..429410cf3c01 100644 --- a/internal/httpclient/model_update_recovery_flow_with_link_method.go +++ b/internal/httpclient/model_update_recovery_flow_with_link_method.go @@ -23,6 +23,8 @@ type UpdateRecoveryFlowWithLinkMethod struct { Email string `json:"email"` // Method is the method that should be used for this recovery flow Allowed values are `link` and `code` link RecoveryStrategyLink code RecoveryStrategyCode Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateRecoveryFlowWithLinkMethod instantiates a new UpdateRecoveryFlowWithLinkMethod object @@ -124,6 +126,38 @@ func (o *UpdateRecoveryFlowWithLinkMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRecoveryFlowWithLinkMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRecoveryFlowWithLinkMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRecoveryFlowWithLinkMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRecoveryFlowWithLinkMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateRecoveryFlowWithLinkMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateRecoveryFlowWithLinkMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_registration_flow_body.go b/internal/httpclient/model_update_registration_flow_body.go index 0e36a95f635f..82a578cfc4d3 100644 --- a/internal/httpclient/model_update_registration_flow_body.go +++ b/internal/httpclient/model_update_registration_flow_body.go @@ -20,7 +20,9 @@ import ( type UpdateRegistrationFlowBody struct { UpdateRegistrationFlowWithCodeMethod *UpdateRegistrationFlowWithCodeMethod UpdateRegistrationFlowWithOidcMethod *UpdateRegistrationFlowWithOidcMethod + UpdateRegistrationFlowWithPasskeyMethod *UpdateRegistrationFlowWithPasskeyMethod UpdateRegistrationFlowWithPasswordMethod *UpdateRegistrationFlowWithPasswordMethod + UpdateRegistrationFlowWithProfileMethod *UpdateRegistrationFlowWithProfileMethod UpdateRegistrationFlowWithWebAuthnMethod *UpdateRegistrationFlowWithWebAuthnMethod } @@ -38,6 +40,13 @@ func UpdateRegistrationFlowWithOidcMethodAsUpdateRegistrationFlowBody(v *UpdateR } } +// UpdateRegistrationFlowWithPasskeyMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithPasskeyMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithPasskeyMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithPasskeyMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithPasskeyMethod: v, + } +} + // UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithPasswordMethod wrapped in UpdateRegistrationFlowBody func UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithPasswordMethod) UpdateRegistrationFlowBody { return UpdateRegistrationFlowBody{ @@ -45,6 +54,13 @@ func UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody(v *Upd } } +// UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithProfileMethod wrapped in UpdateRegistrationFlowBody +func UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithProfileMethod) UpdateRegistrationFlowBody { + return UpdateRegistrationFlowBody{ + UpdateRegistrationFlowWithProfileMethod: v, + } +} + // UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithWebAuthnMethod wrapped in UpdateRegistrationFlowBody func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithWebAuthnMethod) UpdateRegistrationFlowBody { return UpdateRegistrationFlowBody{ @@ -55,72 +71,158 @@ func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *Upd // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateRegistrationFlowWithCodeMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithCodeMethod) - if err == nil { - jsonUpdateRegistrationFlowWithCodeMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithCodeMethod) - if string(jsonUpdateRegistrationFlowWithCodeMethod) == "{}" { // empty struct - dst.UpdateRegistrationFlowWithCodeMethod = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'code' + if jsonDict["method"] == "code" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateRegistrationFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateRegistrationFlowWithCodeMethod = nil } - // try to unmarshal data into UpdateRegistrationFlowWithOidcMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithOidcMethod) - if err == nil { - jsonUpdateRegistrationFlowWithOidcMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithOidcMethod) - if string(jsonUpdateRegistrationFlowWithOidcMethod) == "{}" { // empty struct + // check if the discriminator value is 'oidc' + if jsonDict["method"] == "oidc" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithOidcMethod, return on the first match + } else { dst.UpdateRegistrationFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithOidcMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'passkey' + if jsonDict["method"] == "passkey" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithPasskeyMethod, return on the first match } else { - match++ + dst.UpdateRegistrationFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithPasskeyMethod: %s", err.Error()) } - } else { - dst.UpdateRegistrationFlowWithOidcMethod = nil } - // try to unmarshal data into UpdateRegistrationFlowWithPasswordMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithPasswordMethod) - if err == nil { - jsonUpdateRegistrationFlowWithPasswordMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithPasswordMethod) - if string(jsonUpdateRegistrationFlowWithPasswordMethod) == "{}" { // empty struct + // check if the discriminator value is 'password' + if jsonDict["method"] == "password" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithPasswordMethod, return on the first match + } else { dst.UpdateRegistrationFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithPasswordMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'profile' + if jsonDict["method"] == "profile" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match } else { - match++ + dst.UpdateRegistrationFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error()) } - } else { - dst.UpdateRegistrationFlowWithPasswordMethod = nil } - // try to unmarshal data into UpdateRegistrationFlowWithWebAuthnMethod - err = newStrictDecoder(data).Decode(&dst.UpdateRegistrationFlowWithWebAuthnMethod) - if err == nil { - jsonUpdateRegistrationFlowWithWebAuthnMethod, _ := json.Marshal(dst.UpdateRegistrationFlowWithWebAuthnMethod) - if string(jsonUpdateRegistrationFlowWithWebAuthnMethod) == "{}" { // empty struct + // check if the discriminator value is 'webauthn' + if jsonDict["method"] == "webauthn" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithWebAuthnMethod, return on the first match + } else { dst.UpdateRegistrationFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithWebAuthnMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateRegistrationFlowWithCodeMethod' + if jsonDict["method"] == "updateRegistrationFlowWithCodeMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateRegistrationFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateRegistrationFlowWithWebAuthnMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateRegistrationFlowWithCodeMethod = nil - dst.UpdateRegistrationFlowWithOidcMethod = nil - dst.UpdateRegistrationFlowWithPasswordMethod = nil - dst.UpdateRegistrationFlowWithWebAuthnMethod = nil + // check if the discriminator value is 'updateRegistrationFlowWithOidcMethod' + if jsonDict["method"] == "updateRegistrationFlowWithOidcMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithOidcMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithOidcMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateRegistrationFlowWithPasskeyMethod' + if jsonDict["method"] == "updateRegistrationFlowWithPasskeyMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithPasskeyMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithPasskeyMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateRegistrationFlowWithPasswordMethod' + if jsonDict["method"] == "updateRegistrationFlowWithPasswordMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithPasswordMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithPasswordMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateRegistrationFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateRegistrationFlowBody)") + // check if the discriminator value is 'updateRegistrationFlowWithProfileMethod' + if jsonDict["method"] == "updateRegistrationFlowWithProfileMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error()) + } } + + // check if the discriminator value is 'updateRegistrationFlowWithWebAuthnMethod' + if jsonDict["method"] == "updateRegistrationFlowWithWebAuthnMethod" { + // try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateRegistrationFlowWithWebAuthnMethod, return on the first match + } else { + dst.UpdateRegistrationFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithWebAuthnMethod: %s", err.Error()) + } + } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON @@ -133,10 +235,18 @@ func (src UpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) { return json.Marshal(&src.UpdateRegistrationFlowWithOidcMethod) } + if src.UpdateRegistrationFlowWithPasskeyMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithPasskeyMethod) + } + if src.UpdateRegistrationFlowWithPasswordMethod != nil { return json.Marshal(&src.UpdateRegistrationFlowWithPasswordMethod) } + if src.UpdateRegistrationFlowWithProfileMethod != nil { + return json.Marshal(&src.UpdateRegistrationFlowWithProfileMethod) + } + if src.UpdateRegistrationFlowWithWebAuthnMethod != nil { return json.Marshal(&src.UpdateRegistrationFlowWithWebAuthnMethod) } @@ -157,10 +267,18 @@ func (obj *UpdateRegistrationFlowBody) GetActualInstance() interface{} { return obj.UpdateRegistrationFlowWithOidcMethod } + if obj.UpdateRegistrationFlowWithPasskeyMethod != nil { + return obj.UpdateRegistrationFlowWithPasskeyMethod + } + if obj.UpdateRegistrationFlowWithPasswordMethod != nil { return obj.UpdateRegistrationFlowWithPasswordMethod } + if obj.UpdateRegistrationFlowWithProfileMethod != nil { + return obj.UpdateRegistrationFlowWithProfileMethod + } + if obj.UpdateRegistrationFlowWithWebAuthnMethod != nil { return obj.UpdateRegistrationFlowWithWebAuthnMethod } diff --git a/internal/httpclient/model_update_registration_flow_with_oidc_method.go b/internal/httpclient/model_update_registration_flow_with_oidc_method.go index 509e978a0627..2ee32605fee6 100644 --- a/internal/httpclient/model_update_registration_flow_with_oidc_method.go +++ b/internal/httpclient/model_update_registration_flow_with_oidc_method.go @@ -19,7 +19,7 @@ import ( type UpdateRegistrationFlowWithOidcMethod struct { // The CSRF Token CsrfToken *string `json:"csrf_token,omitempty"` - // IDToken is an optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple + // IDToken is an optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple Google IdToken *string `json:"id_token,omitempty"` // IDTokenNonce is the nonce, used when generating the IDToken. If the provider supports nonce validation, the nonce will be validated against this value and is required. IdTokenNonce *string `json:"id_token_nonce,omitempty"` diff --git a/internal/httpclient/model_update_registration_flow_with_passkey_method.go b/internal/httpclient/model_update_registration_flow_with_passkey_method.go new file mode 100644 index 000000000000..38d59713262e --- /dev/null +++ b/internal/httpclient/model_update_registration_flow_with_passkey_method.go @@ -0,0 +1,249 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateRegistrationFlowWithPasskeyMethod Update Registration Flow with Passkey Method +type UpdateRegistrationFlowWithPasskeyMethod struct { + // CSRFToken is the anti-CSRF token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method Should be set to \"passkey\" when trying to add, update, or remove a Passkey. + Method string `json:"method"` + // Register a WebAuthn Security Key It is expected that the JSON returned by the WebAuthn registration process is included here. + PasskeyRegister *string `json:"passkey_register,omitempty"` + // The identity's traits + Traits map[string]interface{} `json:"traits"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` +} + +// NewUpdateRegistrationFlowWithPasskeyMethod instantiates a new UpdateRegistrationFlowWithPasskeyMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateRegistrationFlowWithPasskeyMethod(method string, traits map[string]interface{}) *UpdateRegistrationFlowWithPasskeyMethod { + this := UpdateRegistrationFlowWithPasskeyMethod{} + this.Method = method + this.Traits = traits + return &this +} + +// NewUpdateRegistrationFlowWithPasskeyMethodWithDefaults instantiates a new UpdateRegistrationFlowWithPasskeyMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateRegistrationFlowWithPasskeyMethodWithDefaults() *UpdateRegistrationFlowWithPasskeyMethod { + this := UpdateRegistrationFlowWithPasskeyMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetMethod(v string) { + o.Method = v +} + +// GetPasskeyRegister returns the PasskeyRegister field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetPasskeyRegister() string { + if o == nil || o.PasskeyRegister == nil { + var ret string + return ret + } + return *o.PasskeyRegister +} + +// GetPasskeyRegisterOk returns a tuple with the PasskeyRegister field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetPasskeyRegisterOk() (*string, bool) { + if o == nil || o.PasskeyRegister == nil { + return nil, false + } + return o.PasskeyRegister, true +} + +// HasPasskeyRegister returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) HasPasskeyRegister() bool { + if o != nil && o.PasskeyRegister != nil { + return true + } + + return false +} + +// SetPasskeyRegister gets a reference to the given string and assigns it to the PasskeyRegister field. +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetPasskeyRegister(v string) { + o.PasskeyRegister = &v +} + +// GetTraits returns the Traits field value +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetTraits() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return nil, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithPasskeyMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithPasskeyMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateRegistrationFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.PasskeyRegister != nil { + toSerialize["passkey_register"] = o.PasskeyRegister + } + if true { + toSerialize["traits"] = o.Traits + } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } + return json.Marshal(toSerialize) +} + +type NullableUpdateRegistrationFlowWithPasskeyMethod struct { + value *UpdateRegistrationFlowWithPasskeyMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithPasskeyMethod) Get() *UpdateRegistrationFlowWithPasskeyMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithPasskeyMethod) Set(val *UpdateRegistrationFlowWithPasskeyMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithPasskeyMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithPasskeyMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithPasskeyMethod(val *UpdateRegistrationFlowWithPasskeyMethod) *NullableUpdateRegistrationFlowWithPasskeyMethod { + return &NullableUpdateRegistrationFlowWithPasskeyMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithPasskeyMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_update_registration_flow_with_profile_method.go b/internal/httpclient/model_update_registration_flow_with_profile_method.go new file mode 100644 index 000000000000..8cdbb2eab764 --- /dev/null +++ b/internal/httpclient/model_update_registration_flow_with_profile_method.go @@ -0,0 +1,249 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateRegistrationFlowWithProfileMethod Update Registration Flow with Profile Method +type UpdateRegistrationFlowWithProfileMethod struct { + // The Anti-CSRF Token This token is only required when performing browser flows. + CsrfToken *string `json:"csrf_token,omitempty"` + // Method Should be set to profile when trying to update a profile. + Method string `json:"method"` + // Screen requests navigation to a previous screen. This must be set to credential-selection to go back to the credential selection screen. credential-selection RegistrationScreenCredentialSelection nolint:gosec // not a credential previous RegistrationScreenPrevious + Screen *string `json:"screen,omitempty"` + // Traits The identity's traits. + Traits map[string]interface{} `json:"traits"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` +} + +// NewUpdateRegistrationFlowWithProfileMethod instantiates a new UpdateRegistrationFlowWithProfileMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateRegistrationFlowWithProfileMethod(method string, traits map[string]interface{}) *UpdateRegistrationFlowWithProfileMethod { + this := UpdateRegistrationFlowWithProfileMethod{} + this.Method = method + this.Traits = traits + return &this +} + +// NewUpdateRegistrationFlowWithProfileMethodWithDefaults instantiates a new UpdateRegistrationFlowWithProfileMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateRegistrationFlowWithProfileMethodWithDefaults() *UpdateRegistrationFlowWithProfileMethod { + this := UpdateRegistrationFlowWithProfileMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithProfileMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateRegistrationFlowWithProfileMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateRegistrationFlowWithProfileMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateRegistrationFlowWithProfileMethod) SetMethod(v string) { + o.Method = v +} + +// GetScreen returns the Screen field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithProfileMethod) GetScreen() string { + if o == nil || o.Screen == nil { + var ret string + return ret + } + return *o.Screen +} + +// GetScreenOk returns a tuple with the Screen field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetScreenOk() (*string, bool) { + if o == nil || o.Screen == nil { + return nil, false + } + return o.Screen, true +} + +// HasScreen returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) HasScreen() bool { + if o != nil && o.Screen != nil { + return true + } + + return false +} + +// SetScreen gets a reference to the given string and assigns it to the Screen field. +func (o *UpdateRegistrationFlowWithProfileMethod) SetScreen(v string) { + o.Screen = &v +} + +// GetTraits returns the Traits field value +func (o *UpdateRegistrationFlowWithProfileMethod) GetTraits() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.Traits +} + +// GetTraitsOk returns a tuple with the Traits field value +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetTraitsOk() (map[string]interface{}, bool) { + if o == nil { + return nil, false + } + return o.Traits, true +} + +// SetTraits sets field value +func (o *UpdateRegistrationFlowWithProfileMethod) SetTraits(v map[string]interface{}) { + o.Traits = v +} + +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithProfileMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithProfileMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateRegistrationFlowWithProfileMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + +func (o UpdateRegistrationFlowWithProfileMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.Screen != nil { + toSerialize["screen"] = o.Screen + } + if true { + toSerialize["traits"] = o.Traits + } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } + return json.Marshal(toSerialize) +} + +type NullableUpdateRegistrationFlowWithProfileMethod struct { + value *UpdateRegistrationFlowWithProfileMethod + isSet bool +} + +func (v NullableUpdateRegistrationFlowWithProfileMethod) Get() *UpdateRegistrationFlowWithProfileMethod { + return v.value +} + +func (v *NullableUpdateRegistrationFlowWithProfileMethod) Set(val *UpdateRegistrationFlowWithProfileMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateRegistrationFlowWithProfileMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateRegistrationFlowWithProfileMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateRegistrationFlowWithProfileMethod(val *UpdateRegistrationFlowWithProfileMethod) *NullableUpdateRegistrationFlowWithProfileMethod { + return &NullableUpdateRegistrationFlowWithProfileMethod{value: val, isSet: true} +} + +func (v NullableUpdateRegistrationFlowWithProfileMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateRegistrationFlowWithProfileMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_update_settings_flow_body.go b/internal/httpclient/model_update_settings_flow_body.go index 064ff9b771dd..cb1edae41d31 100644 --- a/internal/httpclient/model_update_settings_flow_body.go +++ b/internal/httpclient/model_update_settings_flow_body.go @@ -20,6 +20,7 @@ import ( type UpdateSettingsFlowBody struct { UpdateSettingsFlowWithLookupMethod *UpdateSettingsFlowWithLookupMethod UpdateSettingsFlowWithOidcMethod *UpdateSettingsFlowWithOidcMethod + UpdateSettingsFlowWithPasskeyMethod *UpdateSettingsFlowWithPasskeyMethod UpdateSettingsFlowWithPasswordMethod *UpdateSettingsFlowWithPasswordMethod UpdateSettingsFlowWithProfileMethod *UpdateSettingsFlowWithProfileMethod UpdateSettingsFlowWithTotpMethod *UpdateSettingsFlowWithTotpMethod @@ -40,6 +41,13 @@ func UpdateSettingsFlowWithOidcMethodAsUpdateSettingsFlowBody(v *UpdateSettingsF } } +// UpdateSettingsFlowWithPasskeyMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithPasskeyMethod wrapped in UpdateSettingsFlowBody +func UpdateSettingsFlowWithPasskeyMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithPasskeyMethod) UpdateSettingsFlowBody { + return UpdateSettingsFlowBody{ + UpdateSettingsFlowWithPasskeyMethod: v, + } +} + // UpdateSettingsFlowWithPasswordMethodAsUpdateSettingsFlowBody is a convenience function that returns UpdateSettingsFlowWithPasswordMethod wrapped in UpdateSettingsFlowBody func UpdateSettingsFlowWithPasswordMethodAsUpdateSettingsFlowBody(v *UpdateSettingsFlowWithPasswordMethod) UpdateSettingsFlowBody { return UpdateSettingsFlowBody{ @@ -71,100 +79,182 @@ func UpdateSettingsFlowWithWebAuthnMethodAsUpdateSettingsFlowBody(v *UpdateSetti // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateSettingsFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateSettingsFlowWithLookupMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithLookupMethod) - if err == nil { - jsonUpdateSettingsFlowWithLookupMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithLookupMethod) - if string(jsonUpdateSettingsFlowWithLookupMethod) == "{}" { // empty struct - dst.UpdateSettingsFlowWithLookupMethod = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'lookup_secret' + if jsonDict["method"] == "lookup_secret" { + // try to unmarshal JSON data into UpdateSettingsFlowWithLookupMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithLookupMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithLookupMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithLookupMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithLookupMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithLookupMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithOidcMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithOidcMethod) - if err == nil { - jsonUpdateSettingsFlowWithOidcMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithOidcMethod) - if string(jsonUpdateSettingsFlowWithOidcMethod) == "{}" { // empty struct + // check if the discriminator value is 'oidc' + if jsonDict["method"] == "oidc" { + // try to unmarshal JSON data into UpdateSettingsFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithOidcMethod, return on the first match + } else { dst.UpdateSettingsFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithOidcMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'passkey' + if jsonDict["method"] == "passkey" { + // try to unmarshal JSON data into UpdateSettingsFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithPasskeyMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasskeyMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithOidcMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithPasswordMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithPasswordMethod) - if err == nil { - jsonUpdateSettingsFlowWithPasswordMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithPasswordMethod) - if string(jsonUpdateSettingsFlowWithPasswordMethod) == "{}" { // empty struct - dst.UpdateSettingsFlowWithPasswordMethod = nil + // check if the discriminator value is 'password' + if jsonDict["method"] == "password" { + // try to unmarshal JSON data into UpdateSettingsFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithPasswordMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasswordMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithPasswordMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithProfileMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithProfileMethod) - if err == nil { - jsonUpdateSettingsFlowWithProfileMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithProfileMethod) - if string(jsonUpdateSettingsFlowWithProfileMethod) == "{}" { // empty struct - dst.UpdateSettingsFlowWithProfileMethod = nil + // check if the discriminator value is 'profile' + if jsonDict["method"] == "profile" { + // try to unmarshal JSON data into UpdateSettingsFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithProfileMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithProfileMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithProfileMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithTotpMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithTotpMethod) - if err == nil { - jsonUpdateSettingsFlowWithTotpMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithTotpMethod) - if string(jsonUpdateSettingsFlowWithTotpMethod) == "{}" { // empty struct - dst.UpdateSettingsFlowWithTotpMethod = nil + // check if the discriminator value is 'totp' + if jsonDict["method"] == "totp" { + // try to unmarshal JSON data into UpdateSettingsFlowWithTotpMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithTotpMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithTotpMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithTotpMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithTotpMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithTotpMethod = nil } - // try to unmarshal data into UpdateSettingsFlowWithWebAuthnMethod - err = newStrictDecoder(data).Decode(&dst.UpdateSettingsFlowWithWebAuthnMethod) - if err == nil { - jsonUpdateSettingsFlowWithWebAuthnMethod, _ := json.Marshal(dst.UpdateSettingsFlowWithWebAuthnMethod) - if string(jsonUpdateSettingsFlowWithWebAuthnMethod) == "{}" { // empty struct + // check if the discriminator value is 'webauthn' + if jsonDict["method"] == "webauthn" { + // try to unmarshal JSON data into UpdateSettingsFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithWebAuthnMethod, return on the first match + } else { dst.UpdateSettingsFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithWebAuthnMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateSettingsFlowWithLookupMethod' + if jsonDict["method"] == "updateSettingsFlowWithLookupMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithLookupMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithLookupMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithLookupMethod, return on the first match } else { - match++ + dst.UpdateSettingsFlowWithLookupMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithLookupMethod: %s", err.Error()) } - } else { - dst.UpdateSettingsFlowWithWebAuthnMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateSettingsFlowWithLookupMethod = nil - dst.UpdateSettingsFlowWithOidcMethod = nil - dst.UpdateSettingsFlowWithPasswordMethod = nil - dst.UpdateSettingsFlowWithProfileMethod = nil - dst.UpdateSettingsFlowWithTotpMethod = nil - dst.UpdateSettingsFlowWithWebAuthnMethod = nil + // check if the discriminator value is 'updateSettingsFlowWithOidcMethod' + if jsonDict["method"] == "updateSettingsFlowWithOidcMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithOidcMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithOidcMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithOidcMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithOidcMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithOidcMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateSettingsFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateSettingsFlowBody)") + // check if the discriminator value is 'updateSettingsFlowWithPasskeyMethod' + if jsonDict["method"] == "updateSettingsFlowWithPasskeyMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithPasskeyMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithPasskeyMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithPasskeyMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithPasskeyMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasskeyMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateSettingsFlowWithPasswordMethod' + if jsonDict["method"] == "updateSettingsFlowWithPasswordMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithPasswordMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithPasswordMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithPasswordMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithPasswordMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithPasswordMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateSettingsFlowWithProfileMethod' + if jsonDict["method"] == "updateSettingsFlowWithProfileMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithProfileMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithProfileMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithProfileMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithProfileMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithProfileMethod: %s", err.Error()) + } } + + // check if the discriminator value is 'updateSettingsFlowWithTotpMethod' + if jsonDict["method"] == "updateSettingsFlowWithTotpMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithTotpMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithTotpMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithTotpMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithTotpMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithTotpMethod: %s", err.Error()) + } + } + + // check if the discriminator value is 'updateSettingsFlowWithWebAuthnMethod' + if jsonDict["method"] == "updateSettingsFlowWithWebAuthnMethod" { + // try to unmarshal JSON data into UpdateSettingsFlowWithWebAuthnMethod + err = json.Unmarshal(data, &dst.UpdateSettingsFlowWithWebAuthnMethod) + if err == nil { + return nil // data stored in dst.UpdateSettingsFlowWithWebAuthnMethod, return on the first match + } else { + dst.UpdateSettingsFlowWithWebAuthnMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateSettingsFlowBody as UpdateSettingsFlowWithWebAuthnMethod: %s", err.Error()) + } + } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON @@ -177,6 +267,10 @@ func (src UpdateSettingsFlowBody) MarshalJSON() ([]byte, error) { return json.Marshal(&src.UpdateSettingsFlowWithOidcMethod) } + if src.UpdateSettingsFlowWithPasskeyMethod != nil { + return json.Marshal(&src.UpdateSettingsFlowWithPasskeyMethod) + } + if src.UpdateSettingsFlowWithPasswordMethod != nil { return json.Marshal(&src.UpdateSettingsFlowWithPasswordMethod) } @@ -209,6 +303,10 @@ func (obj *UpdateSettingsFlowBody) GetActualInstance() interface{} { return obj.UpdateSettingsFlowWithOidcMethod } + if obj.UpdateSettingsFlowWithPasskeyMethod != nil { + return obj.UpdateSettingsFlowWithPasskeyMethod + } + if obj.UpdateSettingsFlowWithPasswordMethod != nil { return obj.UpdateSettingsFlowWithPasswordMethod } diff --git a/internal/httpclient/model_update_settings_flow_with_lookup_method.go b/internal/httpclient/model_update_settings_flow_with_lookup_method.go index 96a12cb7f9e2..ca2e89827126 100644 --- a/internal/httpclient/model_update_settings_flow_with_lookup_method.go +++ b/internal/httpclient/model_update_settings_flow_with_lookup_method.go @@ -29,6 +29,8 @@ type UpdateSettingsFlowWithLookupMethod struct { LookupSecretReveal *bool `json:"lookup_secret_reveal,omitempty"` // Method Should be set to \"lookup\" when trying to add, update, or remove a lookup pairing. Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateSettingsFlowWithLookupMethod instantiates a new UpdateSettingsFlowWithLookupMethod object @@ -233,6 +235,38 @@ func (o *UpdateSettingsFlowWithLookupMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithLookupMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithLookupMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithLookupMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithLookupMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateSettingsFlowWithLookupMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -253,6 +287,9 @@ func (o UpdateSettingsFlowWithLookupMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_settings_flow_with_oidc_method.go b/internal/httpclient/model_update_settings_flow_with_oidc_method.go index 3008436d8b27..c54a0d1251f3 100644 --- a/internal/httpclient/model_update_settings_flow_with_oidc_method.go +++ b/internal/httpclient/model_update_settings_flow_with_oidc_method.go @@ -25,6 +25,8 @@ type UpdateSettingsFlowWithOidcMethod struct { Method string `json:"method"` // The identity's traits in: body Traits map[string]interface{} `json:"traits,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // Unlink this provider Either this or `link` must be set. type: string in: body Unlink *string `json:"unlink,omitempty"` // UpstreamParameters are the parameters that are passed to the upstream identity provider. These parameters are optional and depend on what the upstream identity provider supports. Supported parameters are: `login_hint` (string): The `login_hint` parameter suppresses the account chooser and either pre-fills the email box on the sign-in form, or selects the proper session. `hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`. `prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`. @@ -169,6 +171,38 @@ func (o *UpdateSettingsFlowWithOidcMethod) SetTraits(v map[string]interface{}) { o.Traits = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithOidcMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithOidcMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithOidcMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithOidcMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetUnlink returns the Unlink field value if set, zero value otherwise. func (o *UpdateSettingsFlowWithOidcMethod) GetUnlink() string { if o == nil || o.Unlink == nil { @@ -247,6 +281,9 @@ func (o UpdateSettingsFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if o.Traits != nil { toSerialize["traits"] = o.Traits } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if o.Unlink != nil { toSerialize["unlink"] = o.Unlink } diff --git a/internal/httpclient/model_update_settings_flow_with_passkey_method.go b/internal/httpclient/model_update_settings_flow_with_passkey_method.go new file mode 100644 index 000000000000..c7103432afcd --- /dev/null +++ b/internal/httpclient/model_update_settings_flow_with_passkey_method.go @@ -0,0 +1,219 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// UpdateSettingsFlowWithPasskeyMethod Update Settings Flow with Passkey Method +type UpdateSettingsFlowWithPasskeyMethod struct { + // CSRFToken is the anti-CSRF token + CsrfToken *string `json:"csrf_token,omitempty"` + // Method Should be set to \"passkey\" when trying to add, update, or remove a webAuthn pairing. + Method string `json:"method"` + // Remove a WebAuthn Security Key This must contain the ID of the WebAuthN connection. + PasskeyRemove *string `json:"passkey_remove,omitempty"` + // Register a WebAuthn Security Key It is expected that the JSON returned by the WebAuthn registration process is included here. + PasskeySettingsRegister *string `json:"passkey_settings_register,omitempty"` +} + +// NewUpdateSettingsFlowWithPasskeyMethod instantiates a new UpdateSettingsFlowWithPasskeyMethod object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateSettingsFlowWithPasskeyMethod(method string) *UpdateSettingsFlowWithPasskeyMethod { + this := UpdateSettingsFlowWithPasskeyMethod{} + this.Method = method + return &this +} + +// NewUpdateSettingsFlowWithPasskeyMethodWithDefaults instantiates a new UpdateSettingsFlowWithPasskeyMethod object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUpdateSettingsFlowWithPasskeyMethodWithDefaults() *UpdateSettingsFlowWithPasskeyMethod { + this := UpdateSettingsFlowWithPasskeyMethod{} + return &this +} + +// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetCsrfToken() string { + if o == nil || o.CsrfToken == nil { + var ret string + return ret + } + return *o.CsrfToken +} + +// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetCsrfTokenOk() (*string, bool) { + if o == nil || o.CsrfToken == nil { + return nil, false + } + return o.CsrfToken, true +} + +// HasCsrfToken returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) HasCsrfToken() bool { + if o != nil && o.CsrfToken != nil { + return true + } + + return false +} + +// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field. +func (o *UpdateSettingsFlowWithPasskeyMethod) SetCsrfToken(v string) { + o.CsrfToken = &v +} + +// GetMethod returns the Method field value +func (o *UpdateSettingsFlowWithPasskeyMethod) GetMethod() string { + if o == nil { + var ret string + return ret + } + + return o.Method +} + +// GetMethodOk returns a tuple with the Method field value +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetMethodOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Method, true +} + +// SetMethod sets field value +func (o *UpdateSettingsFlowWithPasskeyMethod) SetMethod(v string) { + o.Method = v +} + +// GetPasskeyRemove returns the PasskeyRemove field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetPasskeyRemove() string { + if o == nil || o.PasskeyRemove == nil { + var ret string + return ret + } + return *o.PasskeyRemove +} + +// GetPasskeyRemoveOk returns a tuple with the PasskeyRemove field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetPasskeyRemoveOk() (*string, bool) { + if o == nil || o.PasskeyRemove == nil { + return nil, false + } + return o.PasskeyRemove, true +} + +// HasPasskeyRemove returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) HasPasskeyRemove() bool { + if o != nil && o.PasskeyRemove != nil { + return true + } + + return false +} + +// SetPasskeyRemove gets a reference to the given string and assigns it to the PasskeyRemove field. +func (o *UpdateSettingsFlowWithPasskeyMethod) SetPasskeyRemove(v string) { + o.PasskeyRemove = &v +} + +// GetPasskeySettingsRegister returns the PasskeySettingsRegister field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetPasskeySettingsRegister() string { + if o == nil || o.PasskeySettingsRegister == nil { + var ret string + return ret + } + return *o.PasskeySettingsRegister +} + +// GetPasskeySettingsRegisterOk returns a tuple with the PasskeySettingsRegister field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) GetPasskeySettingsRegisterOk() (*string, bool) { + if o == nil || o.PasskeySettingsRegister == nil { + return nil, false + } + return o.PasskeySettingsRegister, true +} + +// HasPasskeySettingsRegister returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasskeyMethod) HasPasskeySettingsRegister() bool { + if o != nil && o.PasskeySettingsRegister != nil { + return true + } + + return false +} + +// SetPasskeySettingsRegister gets a reference to the given string and assigns it to the PasskeySettingsRegister field. +func (o *UpdateSettingsFlowWithPasskeyMethod) SetPasskeySettingsRegister(v string) { + o.PasskeySettingsRegister = &v +} + +func (o UpdateSettingsFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.CsrfToken != nil { + toSerialize["csrf_token"] = o.CsrfToken + } + if true { + toSerialize["method"] = o.Method + } + if o.PasskeyRemove != nil { + toSerialize["passkey_remove"] = o.PasskeyRemove + } + if o.PasskeySettingsRegister != nil { + toSerialize["passkey_settings_register"] = o.PasskeySettingsRegister + } + return json.Marshal(toSerialize) +} + +type NullableUpdateSettingsFlowWithPasskeyMethod struct { + value *UpdateSettingsFlowWithPasskeyMethod + isSet bool +} + +func (v NullableUpdateSettingsFlowWithPasskeyMethod) Get() *UpdateSettingsFlowWithPasskeyMethod { + return v.value +} + +func (v *NullableUpdateSettingsFlowWithPasskeyMethod) Set(val *UpdateSettingsFlowWithPasskeyMethod) { + v.value = val + v.isSet = true +} + +func (v NullableUpdateSettingsFlowWithPasskeyMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableUpdateSettingsFlowWithPasskeyMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUpdateSettingsFlowWithPasskeyMethod(val *UpdateSettingsFlowWithPasskeyMethod) *NullableUpdateSettingsFlowWithPasskeyMethod { + return &NullableUpdateSettingsFlowWithPasskeyMethod{value: val, isSet: true} +} + +func (v NullableUpdateSettingsFlowWithPasskeyMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUpdateSettingsFlowWithPasskeyMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_update_settings_flow_with_password_method.go b/internal/httpclient/model_update_settings_flow_with_password_method.go index d4b9934756f0..450cfdc4fb2b 100644 --- a/internal/httpclient/model_update_settings_flow_with_password_method.go +++ b/internal/httpclient/model_update_settings_flow_with_password_method.go @@ -23,6 +23,8 @@ type UpdateSettingsFlowWithPasswordMethod struct { Method string `json:"method"` // Password is the updated password Password string `json:"password"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateSettingsFlowWithPasswordMethod instantiates a new UpdateSettingsFlowWithPasswordMethod object @@ -124,6 +126,38 @@ func (o *UpdateSettingsFlowWithPasswordMethod) SetPassword(v string) { o.Password = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithPasswordMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithPasswordMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithPasswordMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithPasswordMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateSettingsFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateSettingsFlowWithPasswordMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["password"] = o.Password } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_settings_flow_with_profile_method.go b/internal/httpclient/model_update_settings_flow_with_profile_method.go index af2051f6c38c..f208e2b5fb06 100644 --- a/internal/httpclient/model_update_settings_flow_with_profile_method.go +++ b/internal/httpclient/model_update_settings_flow_with_profile_method.go @@ -23,6 +23,8 @@ type UpdateSettingsFlowWithProfileMethod struct { Method string `json:"method"` // Traits The identity's traits. Traits map[string]interface{} `json:"traits"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateSettingsFlowWithProfileMethod instantiates a new UpdateSettingsFlowWithProfileMethod object @@ -124,6 +126,38 @@ func (o *UpdateSettingsFlowWithProfileMethod) SetTraits(v map[string]interface{} o.Traits = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithProfileMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithProfileMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithProfileMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithProfileMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateSettingsFlowWithProfileMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateSettingsFlowWithProfileMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["traits"] = o.Traits } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_settings_flow_with_totp_method.go b/internal/httpclient/model_update_settings_flow_with_totp_method.go index 565f5d0e8a83..d36d5a00ab53 100644 --- a/internal/httpclient/model_update_settings_flow_with_totp_method.go +++ b/internal/httpclient/model_update_settings_flow_with_totp_method.go @@ -25,6 +25,8 @@ type UpdateSettingsFlowWithTotpMethod struct { TotpCode *string `json:"totp_code,omitempty"` // UnlinkTOTP if true will remove the TOTP pairing, effectively removing the credential. This can be used to set up a new TOTP device. TotpUnlink *bool `json:"totp_unlink,omitempty"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateSettingsFlowWithTotpMethod instantiates a new UpdateSettingsFlowWithTotpMethod object @@ -165,6 +167,38 @@ func (o *UpdateSettingsFlowWithTotpMethod) SetTotpUnlink(v bool) { o.TotpUnlink = &v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithTotpMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithTotpMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithTotpMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithTotpMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateSettingsFlowWithTotpMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -179,6 +213,9 @@ func (o UpdateSettingsFlowWithTotpMethod) MarshalJSON() ([]byte, error) { if o.TotpUnlink != nil { toSerialize["totp_unlink"] = o.TotpUnlink } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_settings_flow_with_web_authn_method.go b/internal/httpclient/model_update_settings_flow_with_web_authn_method.go index 7bcd90c82e58..d09d0def049c 100644 --- a/internal/httpclient/model_update_settings_flow_with_web_authn_method.go +++ b/internal/httpclient/model_update_settings_flow_with_web_authn_method.go @@ -21,6 +21,8 @@ type UpdateSettingsFlowWithWebAuthnMethod struct { CsrfToken *string `json:"csrf_token,omitempty"` // Method Should be set to \"webauthn\" when trying to add, update, or remove a webAuthn pairing. Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // Register a WebAuthn Security Key It is expected that the JSON returned by the WebAuthn registration process is included here. WebauthnRegister *string `json:"webauthn_register,omitempty"` // Name of the WebAuthn Security Key to be Added A human-readable name for the security key which will be added. @@ -103,6 +105,38 @@ func (o *UpdateSettingsFlowWithWebAuthnMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateSettingsFlowWithWebAuthnMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateSettingsFlowWithWebAuthnMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetWebauthnRegister returns the WebauthnRegister field value if set, zero value otherwise. func (o *UpdateSettingsFlowWithWebAuthnMethod) GetWebauthnRegister() string { if o == nil || o.WebauthnRegister == nil { @@ -207,6 +241,9 @@ func (o UpdateSettingsFlowWithWebAuthnMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if o.WebauthnRegister != nil { toSerialize["webauthn_register"] = o.WebauthnRegister } diff --git a/internal/httpclient/model_update_verification_flow_body.go b/internal/httpclient/model_update_verification_flow_body.go index f4e995d222c3..9065bfdbc58e 100644 --- a/internal/httpclient/model_update_verification_flow_body.go +++ b/internal/httpclient/model_update_verification_flow_body.go @@ -39,44 +39,62 @@ func UpdateVerificationFlowWithLinkMethodAsUpdateVerificationFlowBody(v *UpdateV // Unmarshal JSON data into one of the pointers in the struct func (dst *UpdateVerificationFlowBody) UnmarshalJSON(data []byte) error { var err error - match := 0 - // try to unmarshal data into UpdateVerificationFlowWithCodeMethod - err = newStrictDecoder(data).Decode(&dst.UpdateVerificationFlowWithCodeMethod) - if err == nil { - jsonUpdateVerificationFlowWithCodeMethod, _ := json.Marshal(dst.UpdateVerificationFlowWithCodeMethod) - if string(jsonUpdateVerificationFlowWithCodeMethod) == "{}" { // empty struct - dst.UpdateVerificationFlowWithCodeMethod = nil + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + // check if the discriminator value is 'code' + if jsonDict["method"] == "code" { + // try to unmarshal JSON data into UpdateVerificationFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateVerificationFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateVerificationFlowWithCodeMethod, return on the first match } else { - match++ + dst.UpdateVerificationFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithCodeMethod: %s", err.Error()) } - } else { - dst.UpdateVerificationFlowWithCodeMethod = nil } - // try to unmarshal data into UpdateVerificationFlowWithLinkMethod - err = newStrictDecoder(data).Decode(&dst.UpdateVerificationFlowWithLinkMethod) - if err == nil { - jsonUpdateVerificationFlowWithLinkMethod, _ := json.Marshal(dst.UpdateVerificationFlowWithLinkMethod) - if string(jsonUpdateVerificationFlowWithLinkMethod) == "{}" { // empty struct - dst.UpdateVerificationFlowWithLinkMethod = nil + // check if the discriminator value is 'link' + if jsonDict["method"] == "link" { + // try to unmarshal JSON data into UpdateVerificationFlowWithLinkMethod + err = json.Unmarshal(data, &dst.UpdateVerificationFlowWithLinkMethod) + if err == nil { + return nil // data stored in dst.UpdateVerificationFlowWithLinkMethod, return on the first match } else { - match++ + dst.UpdateVerificationFlowWithLinkMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithLinkMethod: %s", err.Error()) } - } else { - dst.UpdateVerificationFlowWithLinkMethod = nil } - if match > 1 { // more than 1 match - // reset to nil - dst.UpdateVerificationFlowWithCodeMethod = nil - dst.UpdateVerificationFlowWithLinkMethod = nil + // check if the discriminator value is 'updateVerificationFlowWithCodeMethod' + if jsonDict["method"] == "updateVerificationFlowWithCodeMethod" { + // try to unmarshal JSON data into UpdateVerificationFlowWithCodeMethod + err = json.Unmarshal(data, &dst.UpdateVerificationFlowWithCodeMethod) + if err == nil { + return nil // data stored in dst.UpdateVerificationFlowWithCodeMethod, return on the first match + } else { + dst.UpdateVerificationFlowWithCodeMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithCodeMethod: %s", err.Error()) + } + } - return fmt.Errorf("Data matches more than one schema in oneOf(UpdateVerificationFlowBody)") - } else if match == 1 { - return nil // exactly one match - } else { // no match - return fmt.Errorf("Data failed to match schemas in oneOf(UpdateVerificationFlowBody)") + // check if the discriminator value is 'updateVerificationFlowWithLinkMethod' + if jsonDict["method"] == "updateVerificationFlowWithLinkMethod" { + // try to unmarshal JSON data into UpdateVerificationFlowWithLinkMethod + err = json.Unmarshal(data, &dst.UpdateVerificationFlowWithLinkMethod) + if err == nil { + return nil // data stored in dst.UpdateVerificationFlowWithLinkMethod, return on the first match + } else { + dst.UpdateVerificationFlowWithLinkMethod = nil + return fmt.Errorf("Failed to unmarshal UpdateVerificationFlowBody as UpdateVerificationFlowWithLinkMethod: %s", err.Error()) + } } + + return nil } // Marshal data from the first non-nil pointers in the struct to JSON diff --git a/internal/httpclient/model_update_verification_flow_with_code_method.go b/internal/httpclient/model_update_verification_flow_with_code_method.go index 4548425684b9..e6821735a296 100644 --- a/internal/httpclient/model_update_verification_flow_with_code_method.go +++ b/internal/httpclient/model_update_verification_flow_with_code_method.go @@ -25,6 +25,8 @@ type UpdateVerificationFlowWithCodeMethod struct { Email *string `json:"email,omitempty"` // Method is the method that should be used for this verification flow Allowed values are `link` and `code`. link VerificationStrategyLink code VerificationStrategyCode Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateVerificationFlowWithCodeMethod instantiates a new UpdateVerificationFlowWithCodeMethod object @@ -165,6 +167,38 @@ func (o *UpdateVerificationFlowWithCodeMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateVerificationFlowWithCodeMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateVerificationFlowWithCodeMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithCodeMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateVerificationFlowWithCodeMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateVerificationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.Code != nil { @@ -179,6 +213,9 @@ func (o UpdateVerificationFlowWithCodeMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_update_verification_flow_with_link_method.go b/internal/httpclient/model_update_verification_flow_with_link_method.go index 8ebbbd749952..b7ab49d3d086 100644 --- a/internal/httpclient/model_update_verification_flow_with_link_method.go +++ b/internal/httpclient/model_update_verification_flow_with_link_method.go @@ -23,6 +23,8 @@ type UpdateVerificationFlowWithLinkMethod struct { Email string `json:"email"` // Method is the method that should be used for this verification flow Allowed values are `link` and `code` link VerificationStrategyLink code VerificationStrategyCode Method string `json:"method"` + // Transient data to pass along to any webhooks + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` } // NewUpdateVerificationFlowWithLinkMethod instantiates a new UpdateVerificationFlowWithLinkMethod object @@ -124,6 +126,38 @@ func (o *UpdateVerificationFlowWithLinkMethod) SetMethod(v string) { o.Method = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *UpdateVerificationFlowWithLinkMethod) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateVerificationFlowWithLinkMethod) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *UpdateVerificationFlowWithLinkMethod) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *UpdateVerificationFlowWithLinkMethod) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + func (o UpdateVerificationFlowWithLinkMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} if o.CsrfToken != nil { @@ -135,6 +169,9 @@ func (o UpdateVerificationFlowWithLinkMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["method"] = o.Method } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_verification_flow.go b/internal/httpclient/model_verification_flow.go index c10870c9f841..ae3039ddee24 100644 --- a/internal/httpclient/model_verification_flow.go +++ b/internal/httpclient/model_verification_flow.go @@ -32,6 +32,8 @@ type VerificationFlow struct { ReturnTo *string `json:"return_to,omitempty"` // State represents the state of this request: choose_method: ask the user to choose a method (e.g. verify your email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the verification challenge was passed. State interface{} `json:"state"` + // TransientPayload is used to pass data from the verification flow to hooks and email templates + TransientPayload map[string]interface{} `json:"transient_payload,omitempty"` // The flow type can either be `api` or `browser`. Type string `json:"type"` Ui UiContainer `json:"ui"` @@ -268,6 +270,38 @@ func (o *VerificationFlow) SetState(v interface{}) { o.State = v } +// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise. +func (o *VerificationFlow) GetTransientPayload() map[string]interface{} { + if o == nil || o.TransientPayload == nil { + var ret map[string]interface{} + return ret + } + return o.TransientPayload +} + +// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *VerificationFlow) GetTransientPayloadOk() (map[string]interface{}, bool) { + if o == nil || o.TransientPayload == nil { + return nil, false + } + return o.TransientPayload, true +} + +// HasTransientPayload returns a boolean if a field has been set. +func (o *VerificationFlow) HasTransientPayload() bool { + if o != nil && o.TransientPayload != nil { + return true + } + + return false +} + +// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field. +func (o *VerificationFlow) SetTransientPayload(v map[string]interface{}) { + o.TransientPayload = v +} + // GetType returns the Type field value func (o *VerificationFlow) GetType() string { if o == nil { @@ -339,6 +373,9 @@ func (o VerificationFlow) MarshalJSON() ([]byte, error) { if o.State != nil { toSerialize["state"] = o.State } + if o.TransientPayload != nil { + toSerialize["transient_payload"] = o.TransientPayload + } if true { toSerialize["type"] = o.Type } diff --git a/internal/httpclient/model_verification_flow_state.go b/internal/httpclient/model_verification_flow_state.go index bea74568c94d..56b65e0c0a5b 100644 --- a/internal/httpclient/model_verification_flow_state.go +++ b/internal/httpclient/model_verification_flow_state.go @@ -16,7 +16,7 @@ import ( "fmt" ) -// VerificationFlowState The state represents the state of the verification flow. choose_method: ask the user to choose a method (e.g. recover account via email) sent_email: the email has been sent to the user passed_challenge: the request was successful and the recovery challenge was passed. +// VerificationFlowState The experimental state represents the state of a verification flow. This field is EXPERIMENTAL and subject to change! type VerificationFlowState string // List of verificationFlowState diff --git a/internal/registrationhelpers/helpers.go b/internal/registrationhelpers/helpers.go index 67a636b567fb..6fd76bd6ef1a 100644 --- a/internal/registrationhelpers/helpers.go +++ b/internal/registrationhelpers/helpers.go @@ -278,6 +278,7 @@ func AssertRegistrationRespectsValidation(t *testing.T, reg *driver.RegistryDefa func AssertCommonErrorCases(t *testing.T, flows []string) { ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, "selfservice.flows.registration.enable_legacy_one_step", true) testhelpers.SetDefaultIdentitySchemaFromRaw(conf, basicSchema) uiTS := testhelpers.NewRegistrationUIFlowEchoServer(t, reg) publicTS := setupServer(t, reg) @@ -287,7 +288,7 @@ func AssertCommonErrorCases(t *testing.T, flows []string) { t.Run("description=can call endpoints only without session", func(t *testing.T) { values := url.Values{} t.Run("type=browser", func(t *testing.T) { - res, err := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg). + res, err := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg). Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(values.Encode()), "application/x-www-form-urlencoded")) require.NoError(t, err) defer res.Body.Close() @@ -296,7 +297,7 @@ func AssertCommonErrorCases(t *testing.T, flows []string) { }) t.Run("type=api", func(t *testing.T) { - res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg). + res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg). Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(testhelpers.EncodeFormAsJSON(t, true, values)), "application/json")) require.NoError(t, err) assert.Len(t, res.Cookies(), 0) @@ -336,7 +337,7 @@ func AssertCommonErrorCases(t *testing.T, flows []string) { values := url.Values{} t.Run("type=browser", func(t *testing.T) { - res, err := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg). + res, err := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg). Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(values.Encode()), "application/x-www-form-urlencoded")) require.NoError(t, err) defer res.Body.Close() @@ -345,7 +346,7 @@ func AssertCommonErrorCases(t *testing.T, flows []string) { }) t.Run("type=api", func(t *testing.T) { - res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg). + res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg). Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(testhelpers.EncodeFormAsJSON(t, true, values)), "application/json")) require.NoError(t, err) assert.Len(t, res.Cookies(), 0) diff --git a/internal/testhelpers/config.go b/internal/testhelpers/config.go index 2a24709c0745..b3450bda72fd 100644 --- a/internal/testhelpers/config.go +++ b/internal/testhelpers/config.go @@ -8,11 +8,12 @@ import ( "encoding/base64" "testing" - "github.com/ory/kratos/driver/config" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" "github.com/spf13/pflag" "github.com/stretchr/testify/require" + "github.com/ory/kratos/driver/config" "github.com/ory/x/configx" "github.com/ory/x/randx" ) @@ -24,6 +25,20 @@ func UseConfigFile(t *testing.T, path string) *pflag.FlagSet { return flags } +func DefaultIdentitySchemaConfig(url string) map[string]any { + return map[string]any{ + config.ViperKeyDefaultIdentitySchemaID: "default", + config.ViperKeyIdentitySchemas: config.Schemas{ + {ID: "default", URL: url}, + }, + } +} + +func WithDefaultIdentitySchema(ctx context.Context, url string) context.Context { + return confighelpers.WithConfigValues(ctx, DefaultIdentitySchemaConfig(url)) +} + +// Deprecated: Use context-based WithDefaultIdentitySchema instead func SetDefaultIdentitySchema(conf *config.Config, url string) func() { schemaUrl, _ := conf.DefaultIdentityTraitsSchemaURL(context.Background()) conf.MustSet(context.Background(), config.ViperKeyDefaultIdentitySchemaID, "default") @@ -37,13 +52,29 @@ func SetDefaultIdentitySchema(conf *config.Config, url string) func() { } } -// UseIdentitySchema registeres an identity schema in the config with a random ID and returns the ID +// WithAddIdentitySchema registers an identity schema in the config with a random ID and returns the ID // -// It also registeres a test cleanup function, to reset the schemas to the original values, after the test finishes +// It also registers a test cleanup function, to reset the schemas to the original values, after the test finishes +func WithAddIdentitySchema(ctx context.Context, t *testing.T, conf *config.Config, url string) (context.Context, string) { + id := randx.MustString(16, randx.Alpha) + schemas, err := conf.IdentityTraitsSchemas(ctx) + require.NoError(t, err) + + return confighelpers.WithConfigValue(ctx, config.ViperKeyIdentitySchemas, append(schemas, config.Schema{ + ID: id, + URL: url, + })), id +} + +// UseIdentitySchema registers an identity schema in the config with a random ID and returns the ID +// +// It also registers a test cleanup function, to reset the schemas to the original values, after the test finishes +// Deprecated: Use context-based WithAddIdentitySchema instead func UseIdentitySchema(t *testing.T, conf *config.Config, url string) (id string) { id = randx.MustString(16, randx.Alpha) schemas, err := conf.IdentityTraitsSchemas(context.Background()) require.NoError(t, err) + conf.MustSet(context.Background(), config.ViperKeyIdentitySchemas, append(schemas, config.Schema{ ID: id, URL: url, @@ -54,7 +85,12 @@ func UseIdentitySchema(t *testing.T, conf *config.Config, url string) (id string return id } -// SetDefaultIdentitySchemaFromRaw allows setting the default identity schema from a raw JSON string. +// WithDefaultIdentitySchemaFromRaw allows setting the default identity schema from a raw JSON string. +func WithDefaultIdentitySchemaFromRaw(ctx context.Context, schema []byte) context.Context { + return WithDefaultIdentitySchema(ctx, "base64://"+base64.URLEncoding.EncodeToString(schema)) +} + +// Deprecated: Use context-based WithDefaultIdentitySchemaFromRaw instead func SetDefaultIdentitySchemaFromRaw(conf *config.Config, schema []byte) { conf.MustSet(context.Background(), config.ViperKeyDefaultIdentitySchemaID, "default") conf.MustSet(context.Background(), config.ViperKeyIdentitySchemas, config.Schemas{ diff --git a/internal/testhelpers/courier.go b/internal/testhelpers/courier.go index 796dac29989d..fcb77f47005d 100644 --- a/internal/testhelpers/courier.go +++ b/internal/testhelpers/courier.go @@ -31,7 +31,7 @@ func CourierExpectMessage(ctx context.Context, t *testing.T, reg interface { }) for _, m := range messages { - if strings.EqualFold(m.Recipient, recipient) && strings.EqualFold(m.Subject, subject) { + if strings.EqualFold(m.Recipient, recipient) && (strings.EqualFold(m.Subject, subject) || strings.Contains(m.Body, subject)) { return &m } } diff --git a/internal/testhelpers/e2e_server.go b/internal/testhelpers/e2e_server.go index 4c97303707b0..b4841d4d080c 100644 --- a/internal/testhelpers/e2e_server.go +++ b/internal/testhelpers/e2e_server.go @@ -216,6 +216,8 @@ func GenerateTLSCertificateFilesForTests(t *testing.T) (certPath, keyPath, certB certOut := io.MultiWriter(enc, certFile) err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) require.NoError(t, err, "Failed to write data to %q: %v", certPath, err) + err = enc.Close() + require.NoError(t, err, "Error closing base64 encoder") err = certFile.Close() require.NoError(t, err, "Error closing %q: %v", certPath, err) certBase64 = buf.String() @@ -234,6 +236,8 @@ func GenerateTLSCertificateFilesForTests(t *testing.T) (certPath, keyPath, certB err = pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) require.NoError(t, err, "Failed to write data to %q: %v", keyPath, err) + err = enc.Close() + require.NoError(t, err, "Error closing base64 encoder") err = keyFile.Close() require.NoError(t, err, "Error closing %q: %v", keyPath, err) keyBase64 = buf.String() diff --git a/internal/testhelpers/handler_mock.go b/internal/testhelpers/handler_mock.go index bcc68a1e61c1..36a51edcc1eb 100644 --- a/internal/testhelpers/handler_mock.go +++ b/internal/testhelpers/handler_mock.go @@ -5,6 +5,7 @@ package testhelpers import ( "context" + "encoding/json" "io" "net/http" "net/http/cookiejar" @@ -26,6 +27,7 @@ import ( type mockDeps interface { identity.PrivilegedPoolProvider + identity.ManagementProvider session.ManagementProvider session.PersistenceProvider config.Provider @@ -34,15 +36,24 @@ type mockDeps interface { func MockSetSession(t *testing.T, reg mockDeps, conf *config.Config) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) - require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) + i.NID = uuid.Must(uuid.NewV4()) + require.NoError(t, i.SetCredentialsWithConfig( + identity.CredentialsTypePassword, + identity.Credentials{ + Type: identity.CredentialsTypePassword, + Identifiers: []string{faker.Email()}, + }, + json.RawMessage(`{"hashed_password":"$"}`))) + require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) MockSetSessionWithIdentity(t, reg, conf, i)(w, r, ps) } } -func MockSetSessionWithIdentity(t *testing.T, reg mockDeps, conf *config.Config, i *identity.Identity) httprouter.Handle { +func MockSetSessionWithIdentity(t *testing.T, reg mockDeps, _ *config.Config, i *identity.Identity) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - activeSession, _ := session.NewActiveSession(r, i, conf, time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + activeSession, err := NewActiveSession(r, reg, i, time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) if aal := r.URL.Query().Get("set_aal"); len(aal) > 0 { activeSession.AuthenticatorAssuranceLevel = identity.AuthenticatorAssuranceLevel(aal) } @@ -52,18 +63,6 @@ func MockSetSessionWithIdentity(t *testing.T, reg mockDeps, conf *config.Config, } } -func MockGetSession(t *testing.T, reg mockDeps) httprouter.Handle { - return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - _, err := reg.SessionManager().FetchFromRequest(r.Context(), r) - if r.URL.Query().Get("has") == "yes" { - require.NoError(t, err) - } else { - require.Error(t, err) - } - w.WriteHeader(http.StatusNoContent) - } -} - func MockMakeAuthenticatedRequest(t *testing.T, reg mockDeps, conf *config.Config, router *httprouter.Router, req *http.Request) ([]byte, *http.Response) { return MockMakeAuthenticatedRequestWithClient(t, reg, conf, router, req, NewClientWithCookies(t)) } diff --git a/internal/testhelpers/http.go b/internal/testhelpers/http.go index 5523de9d5368..f46c1cc62968 100644 --- a/internal/testhelpers/http.go +++ b/internal/testhelpers/http.go @@ -20,26 +20,34 @@ func NewDebugClient(t *testing.T) *http.Client { return &http.Client{Transport: NewTransportWithLogger(http.DefaultTransport, t)} } -func NewClientWithCookieJar(t *testing.T, jar *cookiejar.Jar, debugRedirects bool) *http.Client { +func NewClientWithCookieJar(t *testing.T, jar *cookiejar.Jar, checkRedirect CheckRedirectFunc) *http.Client { if jar == nil { j, err := cookiejar.New(nil) jar = j require.NoError(t, err) } + if checkRedirect == nil { + checkRedirect = DebugRedirects(t) + } return &http.Client{ - Jar: jar, - CheckRedirect: func(req *http.Request, via []*http.Request) error { - if debugRedirects { - t.Logf("Redirect: %s", req.URL.String()) - } - if len(via) >= 20 { - for k, v := range via { - t.Logf("Failed with redirect (%d): %s", k, v.URL.String()) - } - return errors.New("stopped after 20 redirects") + Jar: jar, + CheckRedirect: checkRedirect, + } +} + +type CheckRedirectFunc func(req *http.Request, via []*http.Request) error + +func DebugRedirects(t *testing.T) CheckRedirectFunc { + return func(req *http.Request, via []*http.Request) error { + t.Logf("Redirect: %s", req.URL.String()) + + if len(via) >= 20 { + for k, v := range via { + t.Logf("Failed with redirect (%d): %s", k, v.URL.String()) } - return nil - }, + return errors.New("stopped after 20 redirects") + } + return nil } } diff --git a/internal/testhelpers/identity.go b/internal/testhelpers/identity.go index 5c7cdd5be692..6bbc7c5da27b 100644 --- a/internal/testhelpers/identity.go +++ b/internal/testhelpers/identity.go @@ -19,7 +19,7 @@ func CreateSession(t *testing.T, reg driver.Registry) *session.Session { req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(req.Context(), i)) - sess, err := session.NewActiveSession(req, i, reg.Config(), time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + sess, err := NewActiveSession(req, reg, i, time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) require.NoError(t, err) require.NoError(t, reg.SessionPersister().UpsertSession(req.Context(), sess)) return sess diff --git a/internal/testhelpers/network.go b/internal/testhelpers/network.go index 888f46b583f5..10978dba6b05 100644 --- a/internal/testhelpers/network.go +++ b/internal/testhelpers/network.go @@ -20,7 +20,7 @@ func NewNetworkUnlessExisting(t *testing.T, ctx context.Context, p persistence.P } n := networkx.NewNetwork() - require.NoError(t, p.GetConnection(context.Background()).Create(n)) + require.NoError(t, p.GetConnection(ctx).Create(n)) return n.ID, p.WithNetworkID(n.ID) } diff --git a/internal/testhelpers/sdk.go b/internal/testhelpers/sdk.go index 849f7a73304e..1e55cda04665 100644 --- a/internal/testhelpers/sdk.go +++ b/internal/testhelpers/sdk.go @@ -9,7 +9,6 @@ import ( "net/http/httptest" "net/url" - "github.com/ory/kratos/text" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" @@ -88,26 +87,6 @@ func NewSDKEmailNode(group string) *kratos.UiNode { } } -func NewSDKOIDCNode(name, provider string) *kratos.UiNode { - t := text.NewInfoRegistrationWith(provider) - return &kratos.UiNode{ - Group: node.OpenIDConnectGroup.String(), - Type: "input", - Attributes: kratos.UiNodeInputAttributesAsUiNodeAttributes(&kratos.UiNodeInputAttributes{ - Name: name, - Type: "submit", - Value: provider, - }), - Meta: kratos.UiNodeMeta{ - Label: &kratos.UiText{ - Id: int64(t.ID), - Text: t.Text, - Type: string(t.Type), - }, - }, - } -} - func NewMethodSubmit(group, value string) *kratos.UiNode { return &kratos.UiNode{ Type: "input", diff --git a/internal/testhelpers/selfservice.go b/internal/testhelpers/selfservice.go index 5f31a90232a7..7b76ca3e318e 100644 --- a/internal/testhelpers/selfservice.go +++ b/internal/testhelpers/selfservice.go @@ -11,6 +11,8 @@ import ( "net/url" "testing" + "github.com/gofrs/uuid" + "github.com/go-faker/faker/v4" "github.com/gobuffalo/httptest" "github.com/stretchr/testify/assert" @@ -134,6 +136,7 @@ func SelfServiceHookFakeIdentity(t *testing.T) *identity.Identity { require.NoError(t, faker.FakeData(&i)) i.Traits = identity.Traits(`{}`) i.State = identity.StateActive + i.NID = uuid.Must(uuid.NewV4()) return &i } diff --git a/internal/testhelpers/selfservice_login.go b/internal/testhelpers/selfservice_login.go index 2bd20f81dfd4..d8279ebfeca4 100644 --- a/internal/testhelpers/selfservice_login.go +++ b/internal/testhelpers/selfservice_login.go @@ -59,6 +59,18 @@ type initFlowOptions struct { refresh bool oauth2LoginChallenge string via string + ctx context.Context +} + +func newInitFlowOptions(opts []InitFlowWithOption) *initFlowOptions { + return new(initFlowOptions).apply(opts) +} + +func (o *initFlowOptions) Context() context.Context { + if o.ctx == nil { + return context.Background() + } + return o.ctx } func (o *initFlowOptions) apply(opts []InitFlowWithOption) *initFlowOptions { @@ -116,6 +128,12 @@ func InitFlowWithRefresh() InitFlowWithOption { } } +func InitFlowWithContext(ctx context.Context) InitFlowWithOption { + return func(o *initFlowOptions) { + o.ctx = ctx + } +} + func InitFlowWithOAuth2LoginChallenge(hlc string) InitFlowWithOption { return func(o *initFlowOptions) { o.oauth2LoginChallenge = hlc @@ -134,12 +152,13 @@ func InitializeLoginFlowViaBrowser(t *testing.T, client *http.Client, ts *httpte req, err := http.NewRequest("GET", getURLFromInitOptions(ts, login.RouteInitBrowserFlow, forced, opts...), nil) require.NoError(t, err) + o := newInitFlowOptions(opts) if isSPA { req.Header.Set("Accept", "application/json") } - res, err := client.Do(req) + res, err := client.Do(req.WithContext(o.Context())) require.NoError(t, err) body := x.MustReadAll(res.Body) require.NoError(t, res.Body.Close()) @@ -155,7 +174,7 @@ func InitializeLoginFlowViaBrowser(t *testing.T, client *http.Client, ts *httpte } require.NotEmpty(t, flowID) - rs, r, err := publicClient.FrontendApi.GetLoginFlow(context.Background()).Id(flowID).Execute() + rs, r, err := publicClient.FrontendAPI.GetLoginFlow(context.Background()).Id(flowID).Execute() if expectGetError { require.Error(t, err) require.Nil(t, rs) @@ -167,11 +186,11 @@ func InitializeLoginFlowViaBrowser(t *testing.T, client *http.Client, ts *httpte return rs } -func InitializeLoginFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.Server, forced bool, opts ...InitFlowWithOption) *kratos.LoginFlow { +func InitializeLoginFlowViaAPIWithContext(t *testing.T, ctx context.Context, client *http.Client, ts *httptest.Server, forced bool, opts ...InitFlowWithOption) *kratos.LoginFlow { publicClient := NewSDKCustomClient(ts, client) o := new(initFlowOptions).apply(opts) - req := publicClient.FrontendApi.CreateNativeLoginFlow(context.Background()).Refresh(forced) + req := publicClient.FrontendAPI.CreateNativeLoginFlow(ctx).Refresh(forced) if o.aal != "" { req = req.Aal(string(o.aal)) } @@ -186,6 +205,10 @@ func InitializeLoginFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.S return rs } +func InitializeLoginFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.Server, forced bool, opts ...InitFlowWithOption) *kratos.LoginFlow { + return InitializeLoginFlowViaAPIWithContext(t, context.Background(), client, ts, forced, opts...) +} + func LoginMakeRequest( t *testing.T, isAPI bool, @@ -193,6 +216,18 @@ func LoginMakeRequest( f *kratos.LoginFlow, hc *http.Client, values string, +) (string, *http.Response) { + return LoginMakeRequestWithContext(t, context.Background(), isAPI, isSPA, f, hc, values) +} + +func LoginMakeRequestWithContext( + t *testing.T, + ctx context.Context, + isAPI bool, + isSPA bool, + f *kratos.LoginFlow, + hc *http.Client, + values string, ) (string, *http.Response) { require.NotEmpty(t, f.Ui.Action) @@ -201,7 +236,7 @@ func LoginMakeRequest( req.Header.Set("Accept", "application/json") } - res, err := hc.Do(req) + res, err := hc.Do(req.WithContext(ctx)) require.NoError(t, err, "action: %s", f.Ui.Action) defer res.Body.Close() @@ -210,7 +245,7 @@ func LoginMakeRequest( func GetLoginFlow(t *testing.T, client *http.Client, ts *httptest.Server, flowID string) *kratos.LoginFlow { publicClient := NewSDKCustomClient(ts, client) - rs, _, err := publicClient.FrontendApi.GetLoginFlow(context.Background()).Id(flowID).Execute() + rs, _, err := publicClient.FrontendAPI.GetLoginFlow(context.Background()).Id(flowID).Execute() require.NoError(t, err) return rs } diff --git a/internal/testhelpers/selfservice_recovery.go b/internal/testhelpers/selfservice_recovery.go index c1ff66794553..eaed184ed8ca 100644 --- a/internal/testhelpers/selfservice_recovery.go +++ b/internal/testhelpers/selfservice_recovery.go @@ -43,7 +43,7 @@ func GetVerificationFlow(t *testing.T, client *http.Client, ts *httptest.Server) require.NoError(t, err) require.NoError(t, res.Body.Close()) - rs, _, err := publicClient.FrontendApi.GetVerificationFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute() + rs, _, err := publicClient.FrontendAPI.GetVerificationFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute() require.NoError(t, err, "%s", res.Request.URL.String()) assert.NotEmpty(t, rs.Active) @@ -70,7 +70,7 @@ func InitializeVerificationFlowViaBrowser(t *testing.T, client *http.Client, isS return &f } - rs, _, err := publicClient.FrontendApi.GetVerificationFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute() + rs, _, err := publicClient.FrontendAPI.GetVerificationFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute() require.NoError(t, err) assert.NotEmpty(t, rs.Active) @@ -80,7 +80,7 @@ func InitializeVerificationFlowViaBrowser(t *testing.T, client *http.Client, isS func InitializeVerificationFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.Server) *kratos.VerificationFlow { publicClient := NewSDKCustomClient(ts, client) - rs, _, err := publicClient.FrontendApi.CreateNativeVerificationFlow(context.Background()).Execute() + rs, _, err := publicClient.FrontendAPI.CreateNativeVerificationFlow(context.Background()).Execute() require.NoError(t, err) assert.NotEmpty(t, rs.Active) diff --git a/internal/testhelpers/selfservice_registration.go b/internal/testhelpers/selfservice_registration.go index 0cab8517e541..3f3f900a3000 100644 --- a/internal/testhelpers/selfservice_registration.go +++ b/internal/testhelpers/selfservice_registration.go @@ -65,7 +65,7 @@ func InitializeRegistrationFlowViaBrowser(t *testing.T, client *http.Client, ts flowID = gjson.GetBytes(body, "id").String() } - rs, _, err := NewSDKCustomClient(ts, client).FrontendApi.GetRegistrationFlow(context.Background()).Id(flowID).Execute() + rs, _, err := NewSDKCustomClient(ts, client).FrontendAPI.GetRegistrationFlow(context.Background()).Id(flowID).Execute() if expectGetError { require.Error(t, err) require.Nil(t, rs) @@ -77,14 +77,14 @@ func InitializeRegistrationFlowViaBrowser(t *testing.T, client *http.Client, ts } func InitializeRegistrationFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.Server) *kratos.RegistrationFlow { - rs, _, err := NewSDKCustomClient(ts, client).FrontendApi.CreateNativeRegistrationFlow(context.Background()).Execute() + rs, _, err := NewSDKCustomClient(ts, client).FrontendAPI.CreateNativeRegistrationFlow(context.Background()).Execute() require.NoError(t, err) assert.Empty(t, rs.Active) return rs } func GetRegistrationFlow(t *testing.T, client *http.Client, ts *httptest.Server, flowID string) *kratos.RegistrationFlow { - rs, _, err := NewSDKCustomClient(ts, client).FrontendApi.GetRegistrationFlow(context.Background()).Id(flowID).Execute() + rs, _, err := NewSDKCustomClient(ts, client).FrontendAPI.GetRegistrationFlow(context.Background()).Id(flowID).Execute() require.NoError(t, err) assert.Empty(t, rs.Active) return rs diff --git a/internal/testhelpers/selfservice_settings.go b/internal/testhelpers/selfservice_settings.go index 37a0e97ac472..c46c0eeb757d 100644 --- a/internal/testhelpers/selfservice_settings.go +++ b/internal/testhelpers/selfservice_settings.go @@ -67,8 +67,7 @@ func InitializeSettingsFlowViaBrowser(t *testing.T, client *http.Client, isSPA b require.NoError(t, res.Body.Close()) - rs, res, err := publicClient.FrontendApi.GetSettingsFlow(context.Background()). - Id(flowID).Execute() + rs, res, err := publicClient.FrontendAPI.GetSettingsFlow(context.Background()).Id(flowID).Execute() require.NoError(t, err, "%s", ioutilx.MustReadAll(res.Body)) assert.Empty(t, rs.Active) @@ -78,7 +77,7 @@ func InitializeSettingsFlowViaBrowser(t *testing.T, client *http.Client, isSPA b func InitializeSettingsFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.Server) *kratos.SettingsFlow { publicClient := NewSDKCustomClient(ts, client) - rs, _, err := publicClient.FrontendApi.CreateNativeSettingsFlow(context.Background()).Execute() + rs, _, err := publicClient.FrontendAPI.CreateNativeSettingsFlow(context.Background()).Execute() require.NoError(t, err) assert.Empty(t, rs.Active) @@ -159,7 +158,7 @@ func NewSettingsLoginAcceptAPIServer(t *testing.T, publicClient *kratos.APIClien conf.MustSet(r.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "5m") - res, _, err := publicClient.FrontendApi.GetLoginFlow(context.Background()).Id(r.URL.Query().Get("flow")).Execute() + res, _, err := publicClient.FrontendAPI.GetLoginFlow(context.Background()).Id(r.URL.Query().Get("flow")).Execute() require.NoError(t, err) require.NotEmpty(t, res.RequestUrl) diff --git a/internal/testhelpers/selfservice_verification.go b/internal/testhelpers/selfservice_verification.go index 92bc5b191d43..fd0419d2be61 100644 --- a/internal/testhelpers/selfservice_verification.go +++ b/internal/testhelpers/selfservice_verification.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "encoding/json" + "io" "net/http" "net/http/httptest" "net/url" @@ -29,6 +30,32 @@ import ( "github.com/ory/kratos/x" ) +func NewVerifyAfterHookWebHookTarget(ctx context.Context, t *testing.T, conf *config.Config, assert func(t *testing.T, body []byte)) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + msg, err := io.ReadAll(r.Body) + require.NoError(t, err) + + assert(t, msg) + })) + + // A hook to ensure that the verification hook is called with the correct data + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationAfter+".hooks", []map[string]interface{}{ + { + "hook": "web_hook", + "config": map[string]interface{}{ + "url": ts.URL, + "method": "POST", + "body": "base64://ZnVuY3Rpb24oY3R4KSB7CiAgICBpZGVudGl0eTogY3R4LmlkZW50aXR5Cn0=", + }, + }, + }) + + t.Cleanup(ts.Close) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceVerificationAfter+".hooks", []map[string]interface{}{}) + }) +} + func NewRecoveryUIFlowEchoServer(t *testing.T, reg driver.Registry) *httptest.Server { ctx := context.Background() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -57,6 +84,7 @@ func GetRecoveryFlowForType(t *testing.T, client *http.Client, ts *httptest.Serv res, err := client.Get(url) require.NoError(t, err) + defer res.Body.Close() var flowID string switch ft { @@ -70,7 +98,7 @@ func GetRecoveryFlowForType(t *testing.T, client *http.Client, ts *httptest.Serv } require.NotEmpty(t, flowID, "expected to receive a flow id, got none. %s", ioutilx.MustReadAll(res.Body)) - rs, _, err := publicClient.FrontendApi.GetRecoveryFlow(context.Background()). + rs, _, err := publicClient.FrontendAPI.GetRecoveryFlow(context.Background()). Id(flowID). Execute() require.NoError(t, err, "expected no error when fetching recovery flow: %s", err) @@ -108,7 +136,7 @@ func InitializeRecoveryFlowViaBrowser(t *testing.T, client *http.Client, isSPA b } require.NoError(t, res.Body.Close()) - rs, _, err := publicClient.FrontendApi.GetRecoveryFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute() + rs, _, err := publicClient.FrontendAPI.GetRecoveryFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute() require.NoError(t, err) assert.NotEmpty(t, rs.Active) @@ -118,7 +146,7 @@ func InitializeRecoveryFlowViaBrowser(t *testing.T, client *http.Client, isSPA b func InitializeRecoveryFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.Server) *kratos.RecoveryFlow { publicClient := NewSDKCustomClient(ts, client) - rs, _, err := publicClient.FrontendApi.CreateNativeRecoveryFlow(context.Background()).Execute() + rs, _, err := publicClient.FrontendAPI.CreateNativeRecoveryFlow(context.Background()).Execute() require.NoError(t, err) assert.NotEmpty(t, rs.Active) diff --git a/internal/testhelpers/session.go b/internal/testhelpers/session.go index adbc4f81a392..c614f1afe36d 100644 --- a/internal/testhelpers/session.go +++ b/internal/testhelpers/session.go @@ -10,6 +10,8 @@ import ( "testing" "time" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/nosurf" "github.com/stretchr/testify/assert" @@ -28,7 +30,7 @@ type SessionLifespanProvider struct { e time.Duration } -func (p *SessionLifespanProvider) SessionLifespan(ctx context.Context) time.Duration { +func (p *SessionLifespanProvider) SessionLifespan(context.Context) time.Duration { return p.e } @@ -42,25 +44,25 @@ func NewSessionClient(t *testing.T, u string) *http.Client { return c } -func maybePersistSession(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) { - id, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), sess.Identity.ID) +func maybePersistSession(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, sess *session.Session) { + id, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, sess.Identity.ID) if err != nil { - require.NoError(t, sess.Identity.SetAvailableAAL(context.Background(), reg.IdentityManager())) - require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), sess.Identity)) - id, err = reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), sess.Identity.ID) + require.NoError(t, sess.Identity.SetAvailableAAL(ctx, reg.IdentityManager())) + require.NoError(t, reg.IdentityManager().Create(ctx, sess.Identity)) + id, err = reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, sess.Identity.ID) require.NoError(t, err) } sess.Identity = id sess.IdentityID = id.ID - require.NoError(t, err, reg.SessionPersister().UpsertSession(context.Background(), sess)) + require.NoError(t, err, reg.SessionPersister().UpsertSession(ctx, sess)) } -func NewHTTPClientWithSessionCookie(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client { - maybePersistSession(t, reg, sess) +func NewHTTPClientWithSessionCookie(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, sess *session.Session) *http.Client { + maybePersistSession(t, ctx, reg, sess) var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.NoError(t, reg.SessionManager().IssueCookie(context.Background(), w, r, sess)) + require.NoError(t, reg.SessionManager().IssueCookie(ctx, w, r, sess)) }) if _, ok := reg.CSRFHandler().(*nosurf.CSRFHandler); ok { @@ -75,11 +77,11 @@ func NewHTTPClientWithSessionCookie(t *testing.T, reg *driver.RegistryDefault, s return c } -func NewHTTPClientWithSessionCookieLocalhost(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client { - maybePersistSession(t, reg, sess) +func NewHTTPClientWithSessionCookieLocalhost(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, sess *session.Session) *http.Client { + maybePersistSession(t, ctx, reg, sess) var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.NoError(t, reg.SessionManager().IssueCookie(context.Background(), w, r, sess)) + require.NoError(t, reg.SessionManager().IssueCookie(ctx, w, r, sess)) }) if _, ok := reg.CSRFHandler().(*nosurf.CSRFHandler); ok { @@ -96,11 +98,11 @@ func NewHTTPClientWithSessionCookieLocalhost(t *testing.T, reg *driver.RegistryD return c } -func NewNoRedirectHTTPClientWithSessionCookie(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client { - maybePersistSession(t, reg, sess) +func NewNoRedirectHTTPClientWithSessionCookie(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, sess *session.Session) *http.Client { + maybePersistSession(t, ctx, reg, sess) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.NoError(t, reg.SessionManager().IssueCookie(context.Background(), w, r, sess)) + require.NoError(t, reg.SessionManager().IssueCookie(ctx, w, r, sess)) })) defer ts.Close() @@ -130,8 +132,8 @@ func (ct *TransportWithLogger) RoundTrip(req *http.Request) (*http.Response, err return ct.RoundTripper.RoundTrip(req) } -func NewHTTPClientWithSessionToken(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client { - maybePersistSession(t, reg, sess) +func NewHTTPClientWithSessionToken(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, sess *session.Session) *http.Client { + maybePersistSession(t, ctx, reg, sess) return &http.Client{ Transport: NewTransportWithHeader(t, http.Header{ @@ -140,88 +142,97 @@ func NewHTTPClientWithSessionToken(t *testing.T, reg *driver.RegistryDefault, se } } -func NewHTTPClientWithArbitrarySessionToken(t *testing.T, reg *driver.RegistryDefault) *http.Client { - req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) - s, err := session.NewActiveSession(req, - &identity.Identity{ID: x.NewUUID(), State: identity.StateActive}, - NewSessionLifespanProvider(time.Hour), +func NewHTTPClientWithArbitrarySessionToken(t *testing.T, ctx context.Context, reg *driver.RegistryDefault) *http.Client { + return NewHTTPClientWithArbitrarySessionTokenAndTraits(t, ctx, reg, nil) +} + +func NewHTTPClientWithArbitrarySessionTokenAndTraits(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, traits identity.Traits) *http.Client { + req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil).WithContext(confighelpers.WithConfigValue(ctx, "session.lifespan", time.Hour)) + s, err := NewActiveSession(req, reg, + &identity.Identity{ID: x.NewUUID(), State: identity.StateActive, Traits: traits, NID: x.NewUUID(), SchemaID: "default"}, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1, ) require.NoError(t, err, "Could not initialize session from identity.") - return NewHTTPClientWithSessionToken(t, reg, s) + return NewHTTPClientWithSessionToken(t, ctx, reg, s) } -func NewHTTPClientWithArbitrarySessionCookie(t *testing.T, reg *driver.RegistryDefault) *http.Client { +func NewHTTPClientWithArbitrarySessionCookie(t *testing.T, ctx context.Context, reg *driver.RegistryDefault) *http.Client { req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) - s, err := session.NewActiveSession(req, - &identity.Identity{ID: x.NewUUID(), State: identity.StateActive, Traits: []byte("{}")}, - NewSessionLifespanProvider(time.Hour), + req = req.WithContext(confighelpers.WithConfigValue(ctx, "session.lifespan", time.Hour)) + id := x.NewUUID() + s, err := NewActiveSession(req, reg, + &identity.Identity{ID: id, State: identity.StateActive, Traits: []byte("{}"), Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Type: "password", Identifiers: []string{id.String()}, Config: []byte(`{"hashed_password":"$2a$04$zvZz1zV"}`)}, + }}, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1, ) require.NoError(t, err, "Could not initialize session from identity.") - return NewHTTPClientWithSessionCookie(t, reg, s) + return NewHTTPClientWithSessionCookie(t, ctx, reg, s) } -func NewNoRedirectHTTPClientWithArbitrarySessionCookie(t *testing.T, reg *driver.RegistryDefault) *http.Client { +func NewNoRedirectHTTPClientWithArbitrarySessionCookie(t *testing.T, ctx context.Context, reg *driver.RegistryDefault) *http.Client { req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) - s, err := session.NewActiveSession(req, - &identity.Identity{ID: x.NewUUID(), State: identity.StateActive}, - NewSessionLifespanProvider(time.Hour), + req = req.WithContext(confighelpers.WithConfigValue(ctx, "session.lifespan", time.Hour)) + id := x.NewUUID() + s, err := NewActiveSession(req, reg, + &identity.Identity{ID: id, State: identity.StateActive, + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Type: "password", Identifiers: []string{id.String()}, Config: []byte(`{"hashed_password":"$2a$04$zvZz1zV"}`)}, + }}, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1, ) require.NoError(t, err, "Could not initialize session from identity.") - return NewNoRedirectHTTPClientWithSessionCookie(t, reg, s) + return NewNoRedirectHTTPClientWithSessionCookie(t, ctx, reg, s) } -func NewHTTPClientWithIdentitySessionCookie(t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client { +func NewHTTPClientWithIdentitySessionCookie(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, id *identity.Identity) *http.Client { req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) - s, err := session.NewActiveSession(req, + req = req.WithContext(confighelpers.WithConfigValue(ctx, "session.lifespan", time.Hour)) + s, err := NewActiveSession(req, reg, id, - NewSessionLifespanProvider(time.Hour), time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1, ) require.NoError(t, err, "Could not initialize session from identity.") - return NewHTTPClientWithSessionCookie(t, reg, s) + return NewHTTPClientWithSessionCookie(t, ctx, reg, s) } -func NewHTTPClientWithIdentitySessionCookieLocalhost(t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client { +func NewHTTPClientWithIdentitySessionCookieLocalhost(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, id *identity.Identity) *http.Client { req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) - s, err := session.NewActiveSession(req, + s, err := NewActiveSession(req, reg, id, - NewSessionLifespanProvider(time.Hour), time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1, ) require.NoError(t, err, "Could not initialize session from identity.") - return NewHTTPClientWithSessionCookieLocalhost(t, reg, s) + return NewHTTPClientWithSessionCookieLocalhost(t, ctx, reg, s) } -func NewHTTPClientWithIdentitySessionToken(t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client { +func NewHTTPClientWithIdentitySessionToken(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, id *identity.Identity) *http.Client { req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) - s, err := session.NewActiveSession(req, + req = req.WithContext(confighelpers.WithConfigValue(ctx, "session.lifespan", time.Hour)) + s, err := NewActiveSession(req, reg, id, - NewSessionLifespanProvider(time.Hour), time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1, ) require.NoError(t, err, "Could not initialize session from identity.") - return NewHTTPClientWithSessionToken(t, reg, s) + return NewHTTPClientWithSessionToken(t, ctx, reg, s) } func EnsureAAL(t *testing.T, c *http.Client, ts *httptest.Server, aal string, methods ...string) { @@ -236,8 +247,8 @@ func EnsureAAL(t *testing.T, c *http.Client, ts *httptest.Server, aal string, me assert.Len(t, gjson.GetBytes(sess, "authentication_methods").Array(), 1+len(methods)) } -func NewAuthorizedTransport(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *TransportWithHeader { - maybePersistSession(t, reg, sess) +func NewAuthorizedTransport(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, sess *session.Session) *TransportWithHeader { + maybePersistSession(t, ctx, reg, sess) return NewTransportWithHeader(t, http.Header{ "Authorization": {"Bearer " + sess.Token}, @@ -259,6 +270,10 @@ type TransportWithHeader struct { h http.Header } +func (ct *TransportWithHeader) GetHeader() http.Header { + return ct.h +} + func (ct *TransportWithHeader) RoundTrip(req *http.Request) (*http.Response, error) { for k := range ct.h { req.Header.Set(k, ct.h.Get(k)) diff --git a/internal/testhelpers/session_active.go b/internal/testhelpers/session_active.go new file mode 100644 index 000000000000..a245abce45e1 --- /dev/null +++ b/internal/testhelpers/session_active.go @@ -0,0 +1,23 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package testhelpers + +import ( + "net/http" + "time" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/session" +) + +func NewActiveSession(r *http.Request, reg interface { + session.ManagementProvider +}, i *identity.Identity, authenticatedAt time.Time, completedLoginFor identity.CredentialsType, completedLoginAAL identity.AuthenticatorAssuranceLevel) (*session.Session, error) { + s := session.NewInactiveSession() + s.CompletedLoginFor(completedLoginFor, completedLoginAAL) + if err := reg.SessionManager().ActivateSession(r, s, i, authenticatedAt); err != nil { + return nil, err + } + return s, nil +} diff --git a/package-lock.json b/package-lock.json index 8f860f034e72..705526d800d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "kratos", "dependencies": { "@openapitools/openapi-generator-cli": "2.7.0", "yamljs": "0.3.0" diff --git a/persistence/reference.go b/persistence/reference.go index 56a7ca1712df..d3ceeb8d26b5 100644 --- a/persistence/reference.go +++ b/persistence/reference.go @@ -7,6 +7,8 @@ import ( "context" "time" + "github.com/ory/kratos/x" + "github.com/ory/kratos/selfservice/sessiontokenexchange" "github.com/ory/x/networkx" @@ -63,7 +65,7 @@ type Persister interface { Migrator() *popx.Migrator MigrationBox() *popx.MigrationBox GetConnection(ctx context.Context) *pop.Connection - Transaction(ctx context.Context, callback func(ctx context.Context, connection *pop.Connection) error) error + x.TransactionalPersister Networker } diff --git a/persistence/sql/batch/.snapshots/Test_buildInsertQueryValues-case=testModel-case=cockroach.json b/persistence/sql/batch/.snapshots/Test_buildInsertQueryValues-case=testModel-case=cockroach.json index 9c8e755cacd5..04c9db394e80 100644 --- a/persistence/sql/batch/.snapshots/Test_buildInsertQueryValues-case=testModel-case=cockroach.json +++ b/persistence/sql/batch/.snapshots/Test_buildInsertQueryValues-case=testModel-case=cockroach.json @@ -1,6 +1,6 @@ [ - "0001-01-01T00:00:00Z", - "0001-01-01T00:00:00Z", + "2023-01-01T00:00:00Z", + "2023-01-01T00:00:00Z", "string", 42, null, diff --git a/persistence/sql/batch/create.go b/persistence/sql/batch/create.go index 38254a3b2a80..30b3c9768a7a 100644 --- a/persistence/sql/batch/create.go +++ b/persistence/sql/batch/create.go @@ -8,23 +8,21 @@ import ( "database/sql" "fmt" "reflect" + "slices" "sort" "strings" "time" + "github.com/gobuffalo/pop/v6" + "github.com/gofrs/uuid" "github.com/jmoiron/sqlx/reflectx" + "github.com/pkg/errors" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "github.com/ory/x/dbal" - - "github.com/gobuffalo/pop/v6" - "github.com/gofrs/uuid" - "github.com/pkg/errors" - "github.com/ory/x/otelx" "github.com/ory/x/sqlcon" - "github.com/ory/x/sqlxx" ) @@ -42,9 +40,32 @@ type ( Tracer *otelx.Tracer Connection *pop.Connection } + + // PartialConflictError represents a partial conflict during [Create]. It always + // wraps a [sqlcon.ErrUniqueViolation], so that the caller can either abort the + // whole transaction, or handle the partial success. + PartialConflictError[T any] struct { + Failed []*T + } ) -func buildInsertQueryArgs[T any](ctx context.Context, dialect string, mapper *reflectx.Mapper, quoter quoter, models []*T) insertQueryArgs { +func (p *PartialConflictError[T]) Error() string { + return fmt.Sprintf("partial conflict error: %d models failed to insert", len(p.Failed)) +} +func (p *PartialConflictError[T]) ErrOrNil() error { + if len(p.Failed) == 0 { + return nil + } + return p +} +func (p *PartialConflictError[T]) Unwrap() error { + if len(p.Failed) == 0 { + return nil + } + return sqlcon.ErrUniqueViolation +} + +func buildInsertQueryArgs[T any](ctx context.Context, models []*T, opts *createOpts) insertQueryArgs { var ( v T model = pop.NewModel(v, ctx) @@ -64,7 +85,7 @@ func buildInsertQueryArgs[T any](ctx context.Context, dialect string, mapper *re sort.Strings(columns) for _, col := range columns { - quotedColumns = append(quotedColumns, quoter.Quote(col)) + quotedColumns = append(quotedColumns, opts.quoter.Quote(col)) } // We generate a list (for every row one) of VALUE statements here that @@ -88,37 +109,36 @@ func buildInsertQueryArgs[T any](ctx context.Context, dialect string, mapper *re continue } - field := mapper.FieldByName(m, columns[k]) + field := opts.mapper.FieldByName(m, columns[k]) val, ok := field.Interface().(uuid.UUID) if !ok { continue } - if val == uuid.Nil && dialect == dbal.DriverCockroachDB { + if val == uuid.Nil && opts.dialect == dbal.DriverCockroachDB && !opts.partialInserts { pl[k] = "gen_random_uuid()" break } } - placeholders = append(placeholders, fmt.Sprintf("(%s)", strings.Join(pl, ", "))) } return insertQueryArgs{ - TableName: quoter.Quote(model.TableName()), + TableName: opts.quoter.Quote(model.TableName()), ColumnsDecl: strings.Join(quotedColumns, ", "), Columns: columns, Placeholders: strings.Join(placeholders, ",\n"), } } -func buildInsertQueryValues[T any](dialect string, mapper *reflectx.Mapper, columns []string, models []*T, nowFunc func() time.Time) (values []any, err error) { +func buildInsertQueryValues[T any](columns []string, models []*T, opts *createOpts) (values []any, err error) { for _, m := range models { m := reflect.ValueOf(m) - now := nowFunc() + now := opts.now() // Append model fields to args for _, c := range columns { - field := mapper.FieldByName(m, c) + field := opts.mapper.FieldByName(m, c) switch c { case "created_at": @@ -130,7 +150,7 @@ func buildInsertQueryValues[T any](dialect string, mapper *reflectx.Mapper, colu case "id": if field.Interface().(uuid.UUID) != uuid.Nil { break // breaks switch, not for - } else if dialect == dbal.DriverCockroachDB { + } else if opts.dialect == dbal.DriverCockroachDB && !opts.partialInserts { // This is a special case: // 1. We're using cockroach // 2. It's the primary key field ("ID") @@ -167,9 +187,51 @@ func buildInsertQueryValues[T any](dialect string, mapper *reflectx.Mapper, colu return values, nil } -// Create batch-inserts the given models into the database using a single INSERT statement. -// The models are either all created or none. -func Create[T any](ctx context.Context, p *TracerConnection, models []*T) (err error) { +type createOpts struct { + partialInserts bool + dialect string + mapper *reflectx.Mapper + quoter quoter + now func() time.Time +} + +type CreateOpts func(*createOpts) + +// WithPartialInserts allows to insert only the models that do not conflict with +// an existing record. WithPartialInserts will also generate the IDs for the +// models before inserting them, so that the successful inserts can be correlated +// with the input models. +// +// In particular, WithPartialInserts does not work with MySQL, because it does +// not support the "RETURNING" clause. +// +// WithPartialInserts does not work with CockroachDB and gen_random_uuid(), +// because then the successful inserts cannot be correlated with the input +// models. Note: gen_random_uuid() will skip the UNIQUE constraint check, which +// needs to hit all regions in a distributed setup. Therefore, WithPartialInserts +// should not be used to insert models for only a single identity. +var WithPartialInserts CreateOpts = func(o *createOpts) { + o.partialInserts = true +} + +func newCreateOpts(conn *pop.Connection, opts ...CreateOpts) *createOpts { + o := new(createOpts) + o.dialect = conn.Dialect.Name() + o.mapper = conn.TX.Mapper + o.quoter = conn.Dialect.(quoter) + o.now = func() time.Time { return time.Now().UTC().Truncate(time.Microsecond) } + for _, f := range opts { + f(o) + } + return o +} + +// Create batch-inserts the given models into the database using a single INSERT +// statement. By default, the models are either all created or none. If +// [WithPartialInserts] is passed as an option, partial inserts are supported, +// and the models that could not be inserted are returned in an +// [PartialConflictError]. +func Create[T any](ctx context.Context, p *TracerConnection, models []*T, opts ...CreateOpts) (err error) { ctx, span := p.Tracer.Tracer().Start(ctx, "persistence.sql.batch.Create", trace.WithAttributes(attribute.Int("count", len(models)))) defer otelx.End(span, &err) @@ -182,13 +244,10 @@ func Create[T any](ctx context.Context, p *TracerConnection, models []*T) (err e model := pop.NewModel(v, ctx) conn := p.Connection - quoter, ok := conn.Dialect.(quoter) - if !ok { - return errors.Errorf("store is not a quoter: %T", conn.Store) - } + options := newCreateOpts(conn, opts...) - queryArgs := buildInsertQueryArgs(ctx, conn.Dialect.Name(), conn.TX.Mapper, quoter, models) - values, err := buildInsertQueryValues(conn.Dialect.Name(), conn.TX.Mapper, queryArgs.Columns, models, func() time.Time { return time.Now().UTC().Truncate(time.Microsecond) }) + queryArgs := buildInsertQueryArgs(ctx, models, options) + values, err := buildInsertQueryValues(queryArgs.Columns, models, options) if err != nil { return err } @@ -196,7 +255,11 @@ func Create[T any](ctx context.Context, p *TracerConnection, models []*T) (err e var returningClause string if conn.Dialect.Name() != dbal.DriverMySQL { // PostgreSQL, CockroachDB, SQLite support RETURNING. - returningClause = fmt.Sprintf("RETURNING %s", model.IDField()) + if options.partialInserts { + returningClause = fmt.Sprintf("ON CONFLICT DO NOTHING RETURNING %s", model.IDField()) + } else { + returningClause = fmt.Sprintf("RETURNING %s", model.IDField()) + } } query := conn.Dialect.TranslateSQL(fmt.Sprintf( @@ -213,66 +276,84 @@ func Create[T any](ctx context.Context, p *TracerConnection, models []*T) (err e } defer rows.Close() + // MySQL, which does not support RETURNING, also does not have ON CONFLICT DO + // NOTHING, meaning that MySQL will always fail the whole transaction on a single + // record conflict. + if conn.Dialect.Name() == dbal.DriverMySQL { + return nil + } + + if options.partialInserts { + return handlePartialInserts(queryArgs, values, models, rows) + } else { + return handleFullInserts(models, rows) + } + +} + +func handleFullInserts[T any](models []*T, rows *sql.Rows) error { // Hydrate the models from the RETURNING clause. - // - // Databases not supporting RETURNING will just return 0 rows. - count := 0 - for rows.Next() { - if err := rows.Err(); err != nil { - return sqlcon.HandleError(err) + for i := 0; rows.Next(); i++ { + var id uuid.UUID + if err := rows.Scan(&id); err != nil { + return errors.WithStack(err) } - - if err := setModelID(rows, pop.NewModel(models[count], ctx)); err != nil { + if err := setModelID(id, models[i]); err != nil { return err } - count++ } - if err := rows.Err(); err != nil { return sqlcon.HandleError(err) } - if err := rows.Close(); err != nil { - return sqlcon.HandleError(err) - } - - return sqlcon.HandleError(err) + return nil } -// setModelID was copy & pasted from pop. It basically sets -// the primary key to the given value read from the SQL row. -func setModelID(row *sql.Rows, model *pop.Model) error { - el := reflect.ValueOf(model.Value).Elem() - fbn := el.FieldByName("ID") - if !fbn.IsValid() { - return errors.New("model does not have a field named id") +func handlePartialInserts[T any](queryArgs insertQueryArgs, values []any, models []*T, rows *sql.Rows) error { + // Hydrate the models from the RETURNING clause. + idsInDB := make(map[uuid.UUID]struct{}) + for rows.Next() { + var id uuid.UUID + if err := rows.Scan(&id); err != nil { + return errors.WithStack(err) + } + idsInDB[id] = struct{}{} + } + if err := rows.Err(); err != nil { + return sqlcon.HandleError(err) } - pkt, err := model.PrimaryKeyType() - if err != nil { - return errors.WithStack(err) + idIdx := slices.Index(queryArgs.Columns, "id") + if idIdx == -1 { + return errors.New("id column not found") + } + var idValues []uuid.UUID + for i := idIdx; i < len(values); i += len(queryArgs.Columns) { + idValues = append(idValues, values[i].(uuid.UUID)) } - switch pkt { - case "UUID": - var id uuid.UUID - if err := row.Scan(&id); err != nil { - return errors.WithStack(err) - } - fbn.Set(reflect.ValueOf(id)) - default: - var id interface{} - if err := row.Scan(&id); err != nil { - return errors.WithStack(err) - } - v := reflect.ValueOf(id) - switch fbn.Kind() { - case reflect.Int, reflect.Int64: - fbn.SetInt(v.Int()) - default: - fbn.Set(reflect.ValueOf(id)) + var partialConflictError PartialConflictError[T] + for i, id := range idValues { + if _, ok := idsInDB[id]; !ok { + partialConflictError.Failed = append(partialConflictError.Failed, models[i]) + } else { + if err := setModelID(id, models[i]); err != nil { + return err + } } } + return partialConflictError.ErrOrNil() +} + +// setModelID sets the id field of the model to the id. +func setModelID(id uuid.UUID, model any) error { + el := reflect.ValueOf(model).Elem() + idField := el.FieldByName("ID") + if !idField.IsValid() { + return errors.New("model does not have a field named id") + } + idField.Set(reflect.ValueOf(id)) + return nil } diff --git a/persistence/sql/batch/create_test.go b/persistence/sql/batch/create_test.go index f5c81664a486..131589478e6e 100644 --- a/persistence/sql/batch/create_test.go +++ b/persistence/sql/batch/create_test.go @@ -9,14 +9,13 @@ import ( "testing" "time" - "github.com/ory/x/dbal" - "github.com/gofrs/uuid" "github.com/jmoiron/sqlx/reflectx" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ory/kratos/identity" + "github.com/ory/x/dbal" "github.com/ory/x/snapshotx" "github.com/ory/x/sqlxx" ) @@ -53,8 +52,11 @@ func Test_buildInsertQueryArgs(t *testing.T) { ctx := context.Background() t.Run("case=testModel", func(t *testing.T) { models := makeModels[testModel]() - mapper := reflectx.NewMapper("db") - args := buildInsertQueryArgs(ctx, "other", mapper, testQuoter{}, models) + opts := &createOpts{ + dialect: "other", + quoter: testQuoter{}, + mapper: reflectx.NewMapper("db")} + args := buildInsertQueryArgs(ctx, models, opts) snapshotx.SnapshotT(t, args) query := fmt.Sprintf("INSERT INTO %s (%s) VALUES\n%s", args.TableName, args.ColumnsDecl, args.Placeholders) @@ -73,22 +75,31 @@ func Test_buildInsertQueryArgs(t *testing.T) { t.Run("case=Identities", func(t *testing.T) { models := makeModels[identity.Identity]() - mapper := reflectx.NewMapper("db") - args := buildInsertQueryArgs(ctx, "other", mapper, testQuoter{}, models) + opts := &createOpts{ + dialect: "other", + quoter: testQuoter{}, + mapper: reflectx.NewMapper("db")} + args := buildInsertQueryArgs(ctx, models, opts) snapshotx.SnapshotT(t, args) }) t.Run("case=RecoveryAddress", func(t *testing.T) { models := makeModels[identity.RecoveryAddress]() - mapper := reflectx.NewMapper("db") - args := buildInsertQueryArgs(ctx, "other", mapper, testQuoter{}, models) + opts := &createOpts{ + dialect: "other", + quoter: testQuoter{}, + mapper: reflectx.NewMapper("db")} + args := buildInsertQueryArgs(ctx, models, opts) snapshotx.SnapshotT(t, args) }) t.Run("case=RecoveryAddress", func(t *testing.T) { models := makeModels[identity.RecoveryAddress]() - mapper := reflectx.NewMapper("db") - args := buildInsertQueryArgs(ctx, "other", mapper, testQuoter{}, models) + opts := &createOpts{ + dialect: "other", + quoter: testQuoter{}, + mapper: reflectx.NewMapper("db")} + args := buildInsertQueryArgs(ctx, models, opts) snapshotx.SnapshotT(t, args) }) @@ -99,8 +110,11 @@ func Test_buildInsertQueryArgs(t *testing.T) { models[k].ID = uuid.FromStringOrNil(fmt.Sprintf("ae0125a9-2786-4ada-82d2-d169cf75047%d", k)) } } - mapper := reflectx.NewMapper("db") - args := buildInsertQueryArgs(ctx, "cockroach", mapper, testQuoter{}, models) + opts := &createOpts{ + dialect: dbal.DriverCockroachDB, + quoter: testQuoter{}, + mapper: reflectx.NewMapper("db")} + args := buildInsertQueryArgs(ctx, models, opts) snapshotx.SnapshotT(t, args) }) } @@ -112,25 +126,38 @@ func Test_buildInsertQueryValues(t *testing.T) { Int: 42, Traits: []byte(`{"foo": "bar"}`), } - mapper := reflectx.NewMapper("db") - nowFunc := func() time.Time { - return time.Time{} + frozenTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) + opts := &createOpts{ + mapper: reflectx.NewMapper("db"), + quoter: testQuoter{}, + now: func() time.Time { return frozenTime }, } + t.Run("case=cockroach", func(t *testing.T) { - values, err := buildInsertQueryValues(dbal.DriverCockroachDB, mapper, []string{"created_at", "updated_at", "id", "string", "int", "null_time_ptr", "traits"}, []*testModel{model}, nowFunc) + opts.dialect = dbal.DriverCockroachDB + values, err := buildInsertQueryValues( + []string{"created_at", "updated_at", "id", "string", "int", "null_time_ptr", "traits"}, + []*testModel{model}, + opts, + ) require.NoError(t, err) snapshotx.SnapshotT(t, values) }) t.Run("case=others", func(t *testing.T) { - values, err := buildInsertQueryValues("other", mapper, []string{"created_at", "updated_at", "id", "string", "int", "null_time_ptr", "traits"}, []*testModel{model}, nowFunc) + opts.dialect = "other" + values, err := buildInsertQueryValues( + []string{"created_at", "updated_at", "id", "string", "int", "null_time_ptr", "traits"}, + []*testModel{model}, + opts, + ) require.NoError(t, err) - assert.NotNil(t, model.CreatedAt) + assert.Equal(t, frozenTime, model.CreatedAt) assert.Equal(t, model.CreatedAt, values[0]) - assert.NotNil(t, model.UpdatedAt) + assert.Equal(t, frozenTime, model.UpdatedAt) assert.Equal(t, model.UpdatedAt, values[1]) assert.NotZero(t, model.ID) diff --git a/persistence/sql/batch/test_persister.go b/persistence/sql/batch/test_persister.go new file mode 100644 index 000000000000..be0e9ac7a4c5 --- /dev/null +++ b/persistence/sql/batch/test_persister.go @@ -0,0 +1,112 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package batch + +import ( + "context" + "errors" + "testing" + + "github.com/gobuffalo/pop/v6" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/persistence" + "github.com/ory/x/dbal" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" +) + +func TestPersister(ctx context.Context, tracer *otelx.Tracer, p persistence.Persister) func(t *testing.T) { + return func(t *testing.T) { + t.Run("method=batch.Create", func(t *testing.T) { + + ident1 := identity.NewIdentity("") + ident1.NID = p.NetworkID(ctx) + ident2 := identity.NewIdentity("") + ident2.NID = p.NetworkID(ctx) + + // Create two identities + _ = p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) error { + conn := &TracerConnection{ + Tracer: tracer, + Connection: tx, + } + + err := Create(ctx, conn, []*identity.Identity{ident1, ident2}) + require.NoError(t, err) + + return nil + }) + + require.NotEqual(t, uuid.Nil, ident1.ID) + require.NotEqual(t, uuid.Nil, ident2.ID) + + // Create conflicting verifiable addresses + addresses := []*identity.VerifiableAddress{{ + Value: "foo.1@bar.de", + IdentityID: ident1.ID, + NID: ident1.NID, + }, { + Value: "foo.2@bar.de", + IdentityID: ident1.ID, + NID: ident1.NID, + }, { + Value: "conflict@bar.de", + IdentityID: ident1.ID, + NID: ident1.NID, + }, { + Value: "foo.3@bar.de", + IdentityID: ident1.ID, + NID: ident1.NID, + }, { + Value: "conflict@bar.de", + IdentityID: ident1.ID, + NID: ident1.NID, + }, { + Value: "foo.4@bar.de", + IdentityID: ident1.ID, + NID: ident1.NID, + }} + + t.Run("case=fails all without partial inserts", func(t *testing.T) { + _ = p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) error { + conn := &TracerConnection{ + Tracer: tracer, + Connection: tx, + } + err := Create(ctx, conn, addresses) + assert.ErrorIs(t, err, sqlcon.ErrUniqueViolation) + if partial := new(PartialConflictError[identity.VerifiableAddress]); errors.As(err, &partial) { + require.NoError(t, partial, "expected no partial error") + } + return err + }) + }) + + t.Run("case=return partial error with partial inserts", func(t *testing.T) { + _ = p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) error { + conn := &TracerConnection{ + Tracer: tracer, + Connection: tx, + } + + err := Create(ctx, conn, addresses, WithPartialInserts) + assert.ErrorIs(t, err, sqlcon.ErrUniqueViolation) + + if conn.Connection.Dialect.Name() != dbal.DriverMySQL { + // MySQL does not support partial errors. + partialErr := new(PartialConflictError[identity.VerifiableAddress]) + require.ErrorAs(t, err, &partialErr) + assert.Len(t, partialErr.Failed, 1) + } + + return nil + }) + }) + }) + } +} diff --git a/persistence/sql/identity/persister_identity.go b/persistence/sql/identity/persister_identity.go index 4a0ed4694c46..60fdd71d1b4f 100644 --- a/persistence/sql/identity/persister_identity.go +++ b/persistence/sql/identity/persister_identity.go @@ -6,12 +6,15 @@ package identity import ( "context" "database/sql" + "encoding/base64" "fmt" "sort" "strings" "sync" "time" + "github.com/ory/kratos/x/events" + "github.com/ory/x/crdbx" "github.com/gobuffalo/pop/v6" @@ -46,7 +49,7 @@ var ( ) type dependencies interface { - schema.IdentityTraitsProvider + schema.IdentitySchemaProvider identity.ValidationProvider x.LoggingProvider config.Provider @@ -244,7 +247,45 @@ func (p *IdentityPersister) FindByCredentialsIdentifier(ctx context.Context, ct return nil, nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The SQL adapter failed to return the appropriate credentials_type \"%s\". This is a bug in the code.", ct)) } - return i.CopyWithoutCredentials(), creds, nil + return i, creds, nil +} + +func (p *IdentityPersister) FindIdentityByWebauthnUserHandle(ctx context.Context, userHandle []byte) (_ *identity.Identity, err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.FindIdentityByWebauthnUserHandle") + defer otelx.End(span, &err) + + var id identity.Identity + + var jsonPath string + switch p.GetConnection(ctx).Dialect.Name() { + case "sqlite", "mysql": + jsonPath = "$.user_handle" + default: + jsonPath = "user_handle" + } + + if err := p.GetConnection(ctx).RawQuery(fmt.Sprintf(` +SELECT identities.* +FROM identities +INNER JOIN identity_credentials + ON identities.id = identity_credentials.identity_id + AND identities.nid = identity_credentials.nid + AND identity_credentials.identity_credential_type_id = ( + SELECT id + FROM identity_credential_types + WHERE name = ? + ) +WHERE identity_credentials.config ->> '%s' = ? AND identity_credentials.config ->> '%s' IS NOT NULL + AND identities.nid = ? +LIMIT 1`, jsonPath, jsonPath), + identity.CredentialsTypeWebAuthn, + base64.StdEncoding.EncodeToString(userHandle), + p.NetworkID(ctx), + ).First(&id); err != nil { + return nil, sqlcon.HandleError(err) + } + + return &id, nil } var credentialsTypes = struct { @@ -291,6 +332,11 @@ func (p *IdentityPersister) createIdentityCredentials(ctx context.Context, conn identifiers []*identity.CredentialIdentifier ) + var opts []batch.CreateOpts + if len(identities) > 1 { + opts = append(opts, batch.WithPartialInserts) + } + for _, ident := range identities { for k := range ident.Credentials { cred := ident.Credentials[k] @@ -316,16 +362,16 @@ func (p *IdentityPersister) createIdentityCredentials(ctx context.Context, conn ident.Credentials[k] = cred } } - if err = batch.Create(ctx, traceConn, credentials); err != nil { + if err = batch.Create(ctx, traceConn, credentials, opts...); err != nil { return err } for _, cred := range credentials { - for _, ids := range cred.Identifiers { + for _, identifier := range cred.Identifiers { // Force case-insensitivity and trimming for identifiers - ids = p.normalizeIdentifier(cred.Type, ids) + identifier = NormalizeIdentifier(cred.Type, identifier) - if ids == "" { + if identifier == "" { return errors.WithStack(herodot.ErrInternalServerError.WithReasonf( "Unable to create identity credentials with missing or empty identifier.")) } @@ -336,7 +382,7 @@ func (p *IdentityPersister) createIdentityCredentials(ctx context.Context, conn } identifiers = append(identifiers, &identity.CredentialIdentifier{ - Identifier: ids, + Identifier: identifier, IdentityCredentialsID: cred.ID, IdentityCredentialsTypeID: ct.ID, NID: p.NetworkID(ctx), @@ -344,7 +390,7 @@ func (p *IdentityPersister) createIdentityCredentials(ctx context.Context, conn } } - if err = batch.Create(ctx, traceConn, identifiers); err != nil { + if err = batch.Create(ctx, traceConn, identifiers, opts...); err != nil { return err } @@ -364,8 +410,12 @@ func (p *IdentityPersister) createVerifiableAddresses(ctx context.Context, conn work = append(work, &id.VerifiableAddresses[i]) } } + var opts []batch.CreateOpts + if len(identities) > 1 { + opts = append(opts, batch.WithPartialInserts) + } - return batch.Create(ctx, &batch.TracerConnection{Tracer: p.r.Tracer(ctx), Connection: conn}, work) + return batch.Create(ctx, &batch.TracerConnection{Tracer: p.r.Tracer(ctx), Connection: conn}, work, opts...) } func updateAssociation[T interface { @@ -444,6 +494,9 @@ func (p *IdentityPersister) normalizeVerifiableAddresses(ctx context.Context, id if v.Verified && (v.VerifiedAt == nil || time.Time(*v.VerifiedAt).IsZero()) { v.VerifiedAt = pointerx.Ptr(sqlxx.NullTime(time.Now())) } + if !v.Verified { + v.VerifiedAt = nil + } id.VerifiableAddresses[k] = v } @@ -473,7 +526,12 @@ func (p *IdentityPersister) createRecoveryAddresses(ctx context.Context, conn *p } } - return batch.Create(ctx, &batch.TracerConnection{Tracer: p.r.Tracer(ctx), Connection: conn}, work) + var opts []batch.CreateOpts + if len(identities) > 1 { + opts = append(opts, batch.WithPartialInserts) + } + + return batch.Create(ctx, &batch.TracerConnection{Tracer: p.r.Tracer(ctx), Connection: conn}, work, opts...) } func (p *IdentityPersister) CountIdentities(ctx context.Context) (n int64, err error) { @@ -502,7 +560,7 @@ func (p *IdentityPersister) CreateIdentity(ctx context.Context, ident *identity. func (p *IdentityPersister) CreateIdentities(ctx context.Context, identities ...*identity.Identity) (err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateIdentities", trace.WithAttributes( - attribute.Int("num_identities", len(identities)), + attribute.Int("identities.count", len(identities)), attribute.Stringer("network.id", p.NetworkID(ctx)))) defer otelx.End(span, &err) @@ -532,12 +590,26 @@ func (p *IdentityPersister) CreateIdentities(ctx context.Context, identities ... } } + var succeededIDs []uuid.UUID + + defer func() { + // Report succeeded identities as created. + for _, identID := range succeededIDs { + span.AddEvent(events.NewIdentityCreated(ctx, identID)) + } + }() + return p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) error { conn := &batch.TracerConnection{ Tracer: p.r.Tracer(ctx), Connection: tx, } + succeededIDs = make([]uuid.UUID, 0, len(identities)) + failedIdentityIDs := make(map[uuid.UUID]struct{}) + + // Don't use batch.WithPartialInserts, because identities have no other + // constraints other than the primary key that could cause conflicts. if err := batch.Create(ctx, conn, identities); err != nil { return sqlcon.HandleError(err) } @@ -545,14 +617,73 @@ func (p *IdentityPersister) CreateIdentities(ctx context.Context, identities ... p.normalizeAllAddressess(ctx, identities...) if err = p.createVerifiableAddresses(ctx, tx, identities...); err != nil { - return sqlcon.HandleError(err) + if paritalErr := new(batch.PartialConflictError[identity.VerifiableAddress]); errors.As(err, &paritalErr) { + for _, k := range paritalErr.Failed { + failedIdentityIDs[k.IdentityID] = struct{}{} + } + } else { + return sqlcon.HandleError(err) + } } if err = p.createRecoveryAddresses(ctx, tx, identities...); err != nil { - return sqlcon.HandleError(err) + if paritalErr := new(batch.PartialConflictError[identity.RecoveryAddress]); errors.As(err, &paritalErr) { + for _, k := range paritalErr.Failed { + failedIdentityIDs[k.IdentityID] = struct{}{} + } + } else { + return sqlcon.HandleError(err) + } } if err = p.createIdentityCredentials(ctx, tx, identities...); err != nil { - return sqlcon.HandleError(err) + if paritalErr := new(batch.PartialConflictError[identity.Credentials]); errors.As(err, &paritalErr) { + for _, k := range paritalErr.Failed { + failedIdentityIDs[k.IdentityID] = struct{}{} + } + + } else if paritalErr := new(batch.PartialConflictError[identity.CredentialIdentifier]); errors.As(err, &paritalErr) { + for _, k := range paritalErr.Failed { + credID := k.IdentityCredentialsID + for _, ident := range identities { + for _, cred := range ident.Credentials { + if cred.ID == credID { + failedIdentityIDs[ident.ID] = struct{}{} + } + } + } + } + } else { + return sqlcon.HandleError(err) + } + } + + // If any of the batch inserts failed on conflict, let's delete the corresponding + // identities and return a list of failed identities in the error. + if len(failedIdentityIDs) > 0 { + partialErr := &identity.CreateIdentitiesError{} + failedIDs := make([]uuid.UUID, 0, len(failedIdentityIDs)) + + for _, ident := range identities { + if _, ok := failedIdentityIDs[ident.ID]; ok { + partialErr.AddFailedIdentity(ident, sqlcon.ErrUniqueViolation) + failedIDs = append(failedIDs, ident.ID) + } else { + succeededIDs = append(succeededIDs, ident.ID) + } + } + // Manually roll back by deleting the identities that were inserted before the + // error occurred. + if err := p.DeleteIdentities(ctx, failedIDs); err != nil { + return sqlcon.HandleError(err) + } + + return partialErr + } else { + // No failures: report all identities as created. + for _, ident := range identities { + succeededIDs = append(succeededIDs, ident.ID) + } } + return nil }) } @@ -763,6 +894,10 @@ func (p *IdentityPersister) ListIdentities(ctx context.Context, params identity. attribute.Stringer("network.id", p.NetworkID(ctx)))...)) defer otelx.End(span, &err) + if _, err := uuid.FromString(paginator.Token().Parse("id")["id"]); err != nil { + return nil, nil, errors.WithStack(x.PageTokenInvalid) + } + nid := p.NetworkID(ctx) var is []identity.Identity @@ -791,14 +926,8 @@ func (p *IdentityPersister) ListIdentities(ctx context.Context, params identity. identifier := params.CredentialsIdentifier identifierOperator := "=" if identifier == "" && params.CredentialsIdentifierSimilar != "" { - identifier = params.CredentialsIdentifierSimilar - identifierOperator = "%" - switch con.Dialect.Name() { - case "postgres", "cockroach": - default: - identifier = "%" + identifier + "%" - identifierOperator = "LIKE" - } + identifier = x.EscapeLikePattern(params.CredentialsIdentifierSimilar) + "%" + identifierOperator = "LIKE" } if len(identifier) > 0 { @@ -820,7 +949,7 @@ func (p *IdentityPersister) ListIdentities(ctx context.Context, params identity. identity.CredentialsTypeOIDC, identifier) } - if params.IdsFilter != nil && len(params.IdsFilter) != 0 { + if len(params.IdsFilter) != 0 { wheres += ` AND identities.id in (?) ` @@ -919,6 +1048,24 @@ func (p *IdentityPersister) ListIdentities(ctx context.Context, params identity. return is, nextPage, nil } +func (p *IdentityPersister) UpdateIdentityColumns(ctx context.Context, i *identity.Identity, columns ...string) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateIdentity", + trace.WithAttributes( + attribute.Stringer("identity.id", i.ID), + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + if err := p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) error { + _, err := tx.Where("id = ? AND nid = ?", i.ID, p.NetworkID(ctx)).UpdateQuery(i, columns...) + return sqlcon.HandleError(err) + }); err != nil { + return err + } + + span.AddEvent(events.NewIdentityUpdated(ctx, i.ID)) + return nil +} + func (p *IdentityPersister) UpdateIdentity(ctx context.Context, i *identity.Identity) (err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.UpdateIdentity", trace.WithAttributes( @@ -931,7 +1078,7 @@ func (p *IdentityPersister) UpdateIdentity(ctx context.Context, i *identity.Iden } i.NID = p.NetworkID(ctx) - return sqlcon.HandleError(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) error { + if err := sqlcon.HandleError(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) error { // This returns "ErrNoRows" if the identity does not exist if err := update.Generic(WithTransaction(ctx, tx), tx, p.r.Tracer(ctx).Tracer(), i); err != nil { return err @@ -956,7 +1103,12 @@ func (p *IdentityPersister) UpdateIdentity(ctx context.Context, i *identity.Iden } return sqlcon.HandleError(p.createIdentityCredentials(ctx, tx, i)) - })) + })); err != nil { + return err + } + + span.AddEvent(events.NewIdentityUpdated(ctx, i.ID)) + return nil } func (p *IdentityPersister) DeleteIdentity(ctx context.Context, id uuid.UUID) (err error) { @@ -966,8 +1118,12 @@ func (p *IdentityPersister) DeleteIdentity(ctx context.Context, id uuid.UUID) (e attribute.Stringer("network.id", p.NetworkID(ctx)))) defer otelx.End(span, &err) + tableName := new(identity.Identity).TableName(ctx) + if p.c.Dialect.Name() == "cockroach" { + tableName += "@primary" + } nid := p.NetworkID(ctx) - count, err := p.GetConnection(ctx).RawQuery(fmt.Sprintf("DELETE FROM %s WHERE id = ? AND nid = ?", new(identity.Identity).TableName(ctx)), + count, err := p.GetConnection(ctx).RawQuery(fmt.Sprintf("DELETE FROM %s WHERE id = ? AND nid = ?", tableName), id, nid, ).ExecWithCount() @@ -977,6 +1133,45 @@ func (p *IdentityPersister) DeleteIdentity(ctx context.Context, id uuid.UUID) (e if count == 0 { return errors.WithStack(sqlcon.ErrNoRows) } + span.AddEvent(events.NewIdentityDeleted(ctx, id)) + return nil +} + +func (p *IdentityPersister) DeleteIdentities(ctx context.Context, ids []uuid.UUID) (err error) { + stringIDs := make([]string, len(ids)) + for k, id := range ids { + stringIDs[k] = id.String() + } + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteIdentites", + trace.WithAttributes( + attribute.StringSlice("identity.ids", stringIDs), + attribute.Stringer("network.id", p.NetworkID(ctx)))) + defer otelx.End(span, &err) + + placeholders := strings.TrimSuffix(strings.Repeat("?, ", len(ids)), ", ") + args := make([]any, 0, len(ids)+1) + for _, id := range ids { + args = append(args, id) + } + args = append(args, p.NetworkID(ctx)) + + tableName := new(identity.Identity).TableName(ctx) + if p.c.Dialect.Name() == "cockroach" { + tableName += "@primary" + } + count, err := p.GetConnection(ctx).RawQuery(fmt.Sprintf( + "DELETE FROM %s WHERE id IN (%s) AND nid = ?", + tableName, + placeholders, + ), + args..., + ).ExecWithCount() + if err != nil { + return sqlcon.HandleError(err) + } + if count != len(ids) { + return errors.WithStack(sqlcon.ErrNoRows) + } return nil } diff --git a/persistence/sql/migratest/migration_test.go b/persistence/sql/migratest/migration_test.go index 93513badc4c7..bf727683248b 100644 --- a/persistence/sql/migratest/migration_test.go +++ b/persistence/sql/migratest/migration_test.go @@ -108,7 +108,7 @@ func TestMigrations_Cockroach(t *testing.T) { func testDatabase(t *testing.T, db string, c *pop.Connection) { ctx := context.Background() - l := logrusx.New("", "", logrusx.ForceLevel(logrus.ErrorLevel)) + l := logrusx.New("", "", logrusx.ForceLevel(logrus.DebugLevel)) t.Logf("Cleaning up before migrations") _ = os.Remove("../migrations/sql/schema.sql") @@ -130,15 +130,14 @@ func testDatabase(t *testing.T, db string, c *pop.Connection) { } t.Logf("URL: %s", url) - t.Run("suite=up", func(t *testing.T) { - tm, err := popx.NewMigrationBox( - os.DirFS("../migrations/sql"), - popx.NewMigrator(c, logrusx.New("", "", logrusx.ForceLevel(logrus.DebugLevel)), nil, 1*time.Minute), - popx.WithTestdata(t, os.DirFS("./testdata")), - ) - require.NoError(t, err) - require.NoError(t, tm.Up(ctx)) - }) + tm, err := popx.NewMigrationBox( + os.DirFS("../migrations/sql"), + popx.NewMigrator(c, l, nil, 1*time.Minute), + popx.WithTestdata(t, os.DirFS("./testdata")), + ) + require.NoError(t, err) + tm.DumpMigrations = true + require.NoError(t, tm.Up(ctx)) t.Run("suite=fixtures", func(t *testing.T) { wg := &sync.WaitGroup{} @@ -423,8 +422,6 @@ func testDatabase(t *testing.T, db string, c *pop.Connection) { }) }) - t.Run("suite=down", func(t *testing.T) { - tm := popx.NewTestMigrator(t, c, os.DirFS("../migrations/sql"), os.DirFS("./testdata"), l) - require.NoError(t, tm.Down(ctx, -1)) - }) + tm.DumpMigrations = false + require.NoError(t, tm.Down(ctx, -1)) } diff --git a/persistence/sql/migrations/sql/20230705000000000002_cookie_flow_request_url.cockroach.down.sql b/persistence/sql/migrations/sql/20230705000000000002_cookie_flow_request_url.cockroach.down.sql deleted file mode 100644 index af709521a07a..000000000000 --- a/persistence/sql/migrations/sql/20230705000000000002_cookie_flow_request_url.cockroach.down.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE selfservice_login_flows ALTER COLUMN request_url TYPE VARCHAR(1024); -ALTER TABLE selfservice_recovery_flows ALTER COLUMN request_url TYPE VARCHAR(1024); -ALTER TABLE selfservice_registration_flows ALTER COLUMN request_url TYPE VARCHAR(1024); -ALTER TABLE selfservice_settings_flows ALTER COLUMN request_url TYPE VARCHAR(1024); -ALTER TABLE selfservice_verification_flows ALTER COLUMN request_url TYPE VARCHAR(1024); diff --git a/persistence/sql/migrations/sql/20230705000000000002_cookie_flow_request_url.down.sql b/persistence/sql/migrations/sql/20230705000000000002_cookie_flow_request_url.down.sql deleted file mode 100644 index 14e0ab5aae32..000000000000 --- a/persistence/sql/migrations/sql/20230705000000000002_cookie_flow_request_url.down.sql +++ /dev/null @@ -1 +0,0 @@ --- nothing to do diff --git a/persistence/sql/migrations/sql/20231108111100000000_credential_types_passkey.down.sql b/persistence/sql/migrations/sql/20231108111100000000_credential_types_passkey.down.sql new file mode 100644 index 000000000000..656e0e5de5fe --- /dev/null +++ b/persistence/sql/migrations/sql/20231108111100000000_credential_types_passkey.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'passkey'; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20231108111100000000_credential_types_passkey.up.sql b/persistence/sql/migrations/sql/20231108111100000000_credential_types_passkey.up.sql new file mode 100644 index 000000000000..7d5e3f0faa1a --- /dev/null +++ b/persistence/sql/migrations/sql/20231108111100000000_credential_types_passkey.up.sql @@ -0,0 +1,3 @@ +INSERT INTO identity_credential_types (id, name) +SELECT '8d0ca544-9bf6-45d3-bd75-0bbb3aeba3c7', 'passkey' +WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'passkey'); \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.cockroach.down.sql b/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.cockroach.down.sql new file mode 100644 index 000000000000..c6e7f9d2939e --- /dev/null +++ b/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX identity_credentials_user_handle_idx; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.cockroach.up.sql b/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.cockroach.up.sql new file mode 100644 index 000000000000..14c295e29c5d --- /dev/null +++ b/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.cockroach.up.sql @@ -0,0 +1,3 @@ +CREATE INVERTED INDEX identity_credentials_user_handle_idx + ON identity_credentials (config) + WHERE config ->> 'user_handle' IS NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.down.sql b/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.down.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.up.sql b/persistence/sql/migrations/sql/20240213095000000000_identity_credentials_user_handle_index.up.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.down.sql b/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.down.sql new file mode 100644 index 000000000000..352396fc888d --- /dev/null +++ b/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.down.sql @@ -0,0 +1,4 @@ +CREATE INDEX IF NOT EXISTS courier_message_dispatches_id_message_id_nid_idx ON courier_message_dispatches (id ASC, message_id ASC, nid ASC); + +DROP INDEX courier_message_dispatches_message_id_idx; +DROP INDEX courier_message_dispatches_nid_idx; diff --git a/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.mysql.down.sql b/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.mysql.down.sql new file mode 100644 index 000000000000..7e45f3f3d284 --- /dev/null +++ b/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.mysql.down.sql @@ -0,0 +1,6 @@ +CREATE INDEX courier_message_dispatches_id_message_id_nid_idx ON courier_message_dispatches (id ASC, message_id ASC, nid ASC); + +-- These can't be removed because of foreign key constraints which disallow index deletion in MySQL. + +-- DROP INDEX courier_message_dispatches_message_id_idx ON courier_message_dispatches; +-- DROP INDEX courier_message_dispatches_nid_idx ON courier_message_dispatches; diff --git a/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.mysql.up.sql b/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.mysql.up.sql new file mode 100644 index 000000000000..c277f4dc5c19 --- /dev/null +++ b/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.mysql.up.sql @@ -0,0 +1,8 @@ +-- Remove unused index +DROP INDEX courier_message_dispatches_id_message_id_nid_idx ON courier_message_dispatches; + +-- For pop eager load +CREATE INDEX courier_message_dispatches_message_id_idx ON courier_message_dispatches (message_id, created_at DESC); + +-- For delete by nid +CREATE INDEX courier_message_dispatches_nid_idx ON courier_message_dispatches (nid); diff --git a/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.up.sql b/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.up.sql new file mode 100644 index 000000000000..46aa3a591eee --- /dev/null +++ b/persistence/sql/migrations/sql/20240214113828000000_courier_dispatch_indices.up.sql @@ -0,0 +1,8 @@ +-- Remove unused index +DROP INDEX courier_message_dispatches_id_message_id_nid_idx; + +-- For pop eager load +CREATE INDEX IF NOT EXISTS courier_message_dispatches_message_id_idx ON courier_message_dispatches (message_id, created_at DESC); + +-- For delete by nid +CREATE INDEX IF NOT EXISTS courier_message_dispatches_nid_idx ON courier_message_dispatches (nid); diff --git a/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.cockroach.down.sql b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.cockroach.down.sql new file mode 100644 index 000000000000..1daec47013bc --- /dev/null +++ b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.cockroach.down.sql @@ -0,0 +1,5 @@ +DROP INDEX IF EXISTS identity_login_codes@identity_login_codes_identity_id_idx; +DROP INDEX IF EXISTS identity_login_codes@identity_login_codes_flow_id_idx; +DROP INDEX IF EXISTS identity_recovery_codes@identity_recovery_codes_flow_id_idx; +DROP INDEX IF EXISTS identity_registration_codes@identity_registration_codes_flow_id_idx; +DROP INDEX IF EXISTS identity_verification_codes@identity_verification_codes_flow_id_idx; diff --git a/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.down.sql b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.down.sql new file mode 100644 index 000000000000..73d7df82baf1 --- /dev/null +++ b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.down.sql @@ -0,0 +1,5 @@ +DROP INDEX IF EXISTS identity_login_codes.identity_login_codes_identity_id_idx; +DROP INDEX IF EXISTS identity_login_codes.identity_login_codes_flow_id_idx; +DROP INDEX IF EXISTS identity_recovery_codes.identity_recovery_codes_flow_id_idx; +DROP INDEX IF EXISTS identity_registration_codes.identity_registration_codes_flow_id_idx; +DROP INDEX IF EXISTS identity_verification_codes.identity_verification_codes_flow_id_idx; diff --git a/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.mysql.down.sql b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.mysql.down.sql new file mode 100644 index 000000000000..838a77061a15 --- /dev/null +++ b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.mysql.down.sql @@ -0,0 +1,6 @@ +ALTER TABLE `identity_recovery_codes` + DROP FOREIGN KEY `identity_recovery_codes_identity_id_fk`, + ADD CONSTRAINT `identity_recovery_tokens_identity_id_fk` FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT; + +ALTER TABLE `identity_login_codes` + DROP FOREIGN KEY `identity_login_codes_identity_id_fk`; diff --git a/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.mysql.up.sql b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.mysql.up.sql new file mode 100644 index 000000000000..c1e35805ff5f --- /dev/null +++ b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.mysql.up.sql @@ -0,0 +1,16 @@ +-- This FK was previously misnamed. +ALTER TABLE `identity_recovery_codes` + DROP FOREIGN KEY `identity_recovery_tokens_identity_id_fk`, + ADD CONSTRAINT `identity_recovery_codes_identity_id_fk` FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT; + +-- Missing FK +ALTER TABLE `identity_login_codes` + ADD CONSTRAINT `identity_login_codes_identity_id_fk` FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT; + +-- MySQL has created the remaining indices automatically together with the foreign key constraints. + +-- CREATE INDEX identity_login_codes_identity_id_idx ON identity_login_codes (identity_id ASC); +-- CREATE INDEX identity_login_codes_flow_id_idx ON identity_login_codes (selfservice_login_flow_id ASC); +-- CREATE INDEX identity_registration_codes_flow_id_idx ON identity_registration_codes (selfservice_registration_flow_id ASC); +-- CREATE INDEX identity_recovery_codes_flow_id_idx ON identity_recovery_codes (selfservice_recovery_flow_id ASC); +-- CREATE INDEX identity_verification_codes_flow_id_idx ON identity_verification_codes (selfservice_verification_flow_id ASC); diff --git a/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.sqlite.up.sql b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.sqlite.up.sql new file mode 100644 index 000000000000..61741fcba534 --- /dev/null +++ b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.sqlite.up.sql @@ -0,0 +1,5 @@ +CREATE INDEX IF NOT EXISTS identity_login_codes_identity_id_idx ON identity_login_codes (identity_id ASC); +CREATE INDEX IF NOT EXISTS identity_login_codes_flow_id_idx ON identity_login_codes (selfservice_login_flow_id ASC); +CREATE INDEX IF NOT EXISTS identity_registration_codes_flow_id_idx ON identity_registration_codes (selfservice_registration_flow_id ASC); +CREATE INDEX IF NOT EXISTS identity_recovery_codes_flow_id_idx ON identity_recovery_codes (selfservice_recovery_flow_id ASC); +CREATE INDEX IF NOT EXISTS identity_verification_codes_flow_id_idx ON identity_verification_codes (selfservice_verification_flow_id ASC); diff --git a/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.up.sql b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.up.sql new file mode 100644 index 000000000000..6c3e7e4c9b8a --- /dev/null +++ b/persistence/sql/migrations/sql/20240221000000000000_identity_recovery_codes_flow_id_idx.up.sql @@ -0,0 +1,8 @@ +ALTER TABLE identity_login_codes + ADD CONSTRAINT identity_login_codes_identity_id_fk FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE CASCADE ON UPDATE RESTRICT; + +CREATE INDEX IF NOT EXISTS identity_login_codes_identity_id_idx ON identity_login_codes (identity_id ASC); +CREATE INDEX IF NOT EXISTS identity_login_codes_flow_id_idx ON identity_login_codes (selfservice_login_flow_id ASC); +CREATE INDEX IF NOT EXISTS identity_registration_codes_flow_id_idx ON identity_registration_codes (selfservice_registration_flow_id ASC); +CREATE INDEX IF NOT EXISTS identity_recovery_codes_flow_id_idx ON identity_recovery_codes (selfservice_recovery_flow_id ASC); +CREATE INDEX IF NOT EXISTS identity_verification_codes_flow_id_idx ON identity_verification_codes (selfservice_verification_flow_id ASC); diff --git a/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.down.sql b/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.down.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.postgres.down.sql b/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.postgres.down.sql new file mode 100644 index 000000000000..70d519fb44bc --- /dev/null +++ b/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.postgres.down.sql @@ -0,0 +1,4 @@ +CREATE EXTENSION IF NOT EXISTS pg_trgm; +CREATE EXTENSION IF NOT EXISTS btree_gin; + +CREATE INDEX identity_credential_identifiers_nid_identifier_gin ON identity_credential_identifiers USING GIN (nid, identifier gin_trgm_ops); diff --git a/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.postgres.up.sql b/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.postgres.up.sql new file mode 100644 index 000000000000..159cb60805d1 --- /dev/null +++ b/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.postgres.up.sql @@ -0,0 +1 @@ +DROP INDEX identity_credential_identifiers_nid_identifier_gin; diff --git a/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.up.sql b/persistence/sql/migrations/sql/20240318143139000000_drop_identity_search_index.up.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.cockroach.down.sql b/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.cockroach.down.sql new file mode 100644 index 000000000000..f692e9943649 --- /dev/null +++ b/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.cockroach.down.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS identity_credential_identifiers_nid_identifier_gin ON identity_credential_identifiers USING GIN (nid, identifier gin_trgm_ops); diff --git a/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.cockroach.up.sql b/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.cockroach.up.sql new file mode 100644 index 000000000000..d1c8cdf26841 --- /dev/null +++ b/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.cockroach.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS identity_credential_identifiers_nid_identifier_gin; diff --git a/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.down.sql b/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.down.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.up.sql b/persistence/sql/migrations/sql/20240325153839000000_drop_identity_search_index.up.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.cockroach.down.sql b/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.cockroach.down.sql new file mode 100644 index 000000000000..dd8f3d45ff55 --- /dev/null +++ b/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.cockroach.down.sql @@ -0,0 +1 @@ +DROP INDEX identity_credentials_config_user_handle_idx; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.cockroach.up.sql b/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.cockroach.up.sql new file mode 100644 index 000000000000..a953dd44b8b1 --- /dev/null +++ b/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.cockroach.up.sql @@ -0,0 +1,4 @@ +CREATE INDEX identity_credentials_config_user_handle_idx + ON identity_credentials ((config ->> 'user_handle')) + WHERE config ->> 'user_handle' IS NOT NULL +; diff --git a/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.down.sql b/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.down.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.up.sql b/persistence/sql/migrations/sql/20240425095000000000_identity_credentials_fix_user_handle_index.up.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.cockroach.down.sql b/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.cockroach.down.sql new file mode 100644 index 000000000000..14c295e29c5d --- /dev/null +++ b/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.cockroach.down.sql @@ -0,0 +1,3 @@ +CREATE INVERTED INDEX identity_credentials_user_handle_idx + ON identity_credentials (config) + WHERE config ->> 'user_handle' IS NOT NULL; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.cockroach.up.sql b/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.cockroach.up.sql new file mode 100644 index 000000000000..91e0c2a6c2c2 --- /dev/null +++ b/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.cockroach.up.sql @@ -0,0 +1 @@ +DROP INDEX identity_credentials_user_handle_idx; diff --git a/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.down.sql b/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.down.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.up.sql b/persistence/sql/migrations/sql/20240425095000000001_identity_credentials_fix_user_handle_index.up.sql new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/persistence/sql/persister.go b/persistence/sql/persister.go index 99ec38f40fc0..f222660e006e 100644 --- a/persistence/sql/persister.go +++ b/persistence/sql/persister.go @@ -40,7 +40,7 @@ type ( config.Provider contextx.Provider x.TracingProvider - schema.IdentityTraitsProvider + schema.IdentitySchemaProvider identity.ValidationProvider } Persister struct { diff --git a/persistence/sql/persister_cleanup_test.go b/persistence/sql/persister_cleanup_test.go index 65e95ea6ea00..efb14a05e6c9 100644 --- a/persistence/sql/persister_cleanup_test.go +++ b/persistence/sql/persister_cleanup_test.go @@ -14,6 +14,8 @@ import ( ) func TestPersister_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() ctx := context.Background() @@ -29,6 +31,8 @@ func TestPersister_Cleanup(t *testing.T) { } func TestPersister_Continuity_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -45,6 +49,8 @@ func TestPersister_Continuity_Cleanup(t *testing.T) { } func TestPersister_Login_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -61,6 +67,8 @@ func TestPersister_Login_Cleanup(t *testing.T) { } func TestPersister_Recovery_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -77,6 +85,8 @@ func TestPersister_Recovery_Cleanup(t *testing.T) { } func TestPersister_Registration_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -93,6 +103,8 @@ func TestPersister_Registration_Cleanup(t *testing.T) { } func TestPersister_Session_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -109,6 +121,8 @@ func TestPersister_Session_Cleanup(t *testing.T) { } func TestPersister_Settings_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -125,6 +139,8 @@ func TestPersister_Settings_Cleanup(t *testing.T) { } func TestPersister_Verification_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() @@ -141,6 +157,8 @@ func TestPersister_Verification_Cleanup(t *testing.T) { } func TestPersister_SessionTokenExchange_Cleanup(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() currentTime := time.Now() diff --git a/persistence/sql/persister_code.go b/persistence/sql/persister_code.go index 31e0b80dc2d2..ece7dea75ec3 100644 --- a/persistence/sql/persister_code.go +++ b/persistence/sql/persister_code.go @@ -94,7 +94,7 @@ func useOneTimeCode[P any, U interface { secrets: for _, secret := range p.r.Config().SecretsSession(ctx) { - suppliedCode := []byte(p.hmacValueWithSecret(ctx, userProvidedCode, secret)) + suppliedCode := []byte(hmacValueWithSecret(ctx, userProvidedCode, secret)) for i := range codes { c := codes[i] if subtle.ConstantTimeCompare([]byte(c.GetHMACCode()), suppliedCode) == 0 { diff --git a/persistence/sql/persister_continuity.go b/persistence/sql/persister_continuity.go index 73078784766c..ee7a4597e469 100644 --- a/persistence/sql/persister_continuity.go +++ b/persistence/sql/persister_continuity.go @@ -28,6 +28,23 @@ func (p *Persister) SaveContinuitySession(ctx context.Context, c *continuity.Con return sqlcon.HandleError(p.GetConnection(ctx).Create(c)) } +func (p *Persister) SetContinuitySessionExpiry(ctx context.Context, id uuid.UUID, expiresAt time.Time) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.SetContinuitySessionExpiry") + defer otelx.End(span, &err) + + if rows, err := p.GetConnection(ctx). + Where("id = ? AND nid = ?", id, p.NetworkID(ctx)). + UpdateQuery(&continuity.Container{ + ExpiresAt: expiresAt, + }, "expires_at"); err != nil { + return sqlcon.HandleError(err) + } else if rows == 0 { + return errors.WithStack(sqlcon.ErrNoRows) + } + + return nil +} + func (p *Persister) GetContinuitySession(ctx context.Context, id uuid.UUID) (_ *continuity.Container, err error) { ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetContinuitySession") defer otelx.End(span, &err) diff --git a/persistence/sql/persister_courier.go b/persistence/sql/persister_courier.go index 456efea4fe70..ec9694924f6e 100644 --- a/persistence/sql/persister_courier.go +++ b/persistence/sql/persister_courier.go @@ -20,6 +20,7 @@ import ( "github.com/ory/kratos/courier" "github.com/ory/kratos/persistence/sql/update" + "github.com/ory/kratos/x" ) var _ courier.Persister = new(Persister) @@ -57,6 +58,10 @@ func (p *Persister) ListMessages(ctx context.Context, filter courier.ListCourier opts = append(opts, keysetpagination.WithColumn("created_at", "DESC")) paginator := keysetpagination.GetPaginator(opts...) + if _, err := uuid.FromString(paginator.Token().Parse("id")["id"]); err != nil { + return nil, 0, nil, errors.WithStack(x.PageTokenInvalid) + } + messages := make([]courier.Message, paginator.Size()) if err := q.Scope(keysetpagination.Paginate[courier.Message](paginator)). All(&messages); err != nil { diff --git a/persistence/sql/persister_errorx.go b/persistence/sql/persister_errorx.go index 15faf9fd163b..fc656074f0fc 100644 --- a/persistence/sql/persister_errorx.go +++ b/persistence/sql/persister_errorx.go @@ -4,18 +4,17 @@ package sql import ( - "bytes" "context" "encoding/json" "time" + "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" "github.com/pkg/errors" "go.opentelemetry.io/otel/attribute" - "github.com/ory/jsonschema/v3" - "github.com/ory/herodot" + "github.com/ory/jsonschema/v3" "github.com/ory/x/otelx" "github.com/ory/x/sqlcon" @@ -28,7 +27,7 @@ func (p *Persister) CreateErrorContainer(ctx context.Context, csrfToken string, ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateErrorContainer") defer otelx.End(span, &err) - message, err := p.encodeSelfServiceErrors(ctx, errs) + message, err := encodeSelfServiceErrors(errs) if err != nil { return uuid.Nil, err } @@ -55,14 +54,19 @@ func (p *Persister) ReadErrorContainer(ctx context.Context, id uuid.UUID) (_ *er defer otelx.End(span, &err) var ec errorx.ErrorContainer - if err := p.GetConnection(ctx).Where("id = ? AND nid = ?", id, p.NetworkID(ctx)).First(&ec); err != nil { - return nil, sqlcon.HandleError(err) - } - - if err := p.GetConnection(ctx).RawQuery( - "UPDATE selfservice_errors SET was_seen = true, seen_at = ? WHERE id = ? AND nid = ?", - time.Now().UTC(), id, p.NetworkID(ctx)).Exec(); err != nil { - return nil, sqlcon.HandleError(err) + if err := p.Transaction(ctx, func(ctx context.Context, c *pop.Connection) error { + if err := c.Where("id = ? AND nid = ?", id, p.NetworkID(ctx)).First(&ec); err != nil { + return sqlcon.HandleError(err) + } + + if err := c.RawQuery( + "UPDATE selfservice_errors SET was_seen = true, seen_at = ? WHERE id = ? AND nid = ?", + time.Now().UTC(), id, p.NetworkID(ctx)).Exec(); err != nil { + return sqlcon.HandleError(err) + } + return nil + }); err != nil { + return nil, err } return &ec, nil @@ -85,7 +89,7 @@ func (p *Persister) ClearErrorContainers(ctx context.Context, olderThan time.Dur return sqlcon.HandleError(err) } -func (p *Persister) encodeSelfServiceErrors(ctx context.Context, e error) ([]byte, error) { +func encodeSelfServiceErrors(e error) ([]byte, error) { if e == nil { return nil, errors.WithStack(herodot.ErrInternalServerError.WithDebug("A nil error was passed to the error manager which is most likely a code bug.")) } @@ -98,10 +102,10 @@ func (p *Persister) encodeSelfServiceErrors(ctx context.Context, e error) ([]byt e = herodot.ToDefaultError(e, "") } - var b bytes.Buffer - if err := json.NewEncoder(&b).Encode(e); err != nil { + enc, err := json.Marshal(e) + if err != nil { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to encode error messages.").WithDebug(err.Error())) } - return b.Bytes(), nil + return enc, nil } diff --git a/persistence/sql/persister_hmac.go b/persistence/sql/persister_hmac.go index 9c4d6636f14c..8fdb04df3dd6 100644 --- a/persistence/sql/persister_hmac.go +++ b/persistence/sql/persister_hmac.go @@ -7,27 +7,19 @@ import ( "context" "crypto/hmac" "crypto/sha512" - "crypto/subtle" "fmt" + + "go.opentelemetry.io/otel/trace" ) func (p *Persister) hmacValue(ctx context.Context, value string) string { - return p.hmacValueWithSecret(ctx, value, p.r.Config().SecretsSession(ctx)[0]) + return hmacValueWithSecret(ctx, value, p.r.Config().SecretsSession(ctx)[0]) } -func (p *Persister) hmacValueWithSecret(ctx context.Context, value string, secret []byte) string { - _, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.hmacValueWithSecret") +func hmacValueWithSecret(ctx context.Context, value string, secret []byte) string { + _, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, "persistence.sql.hmacValueWithSecret") defer span.End() h := hmac.New(sha512.New512_256, secret) _, _ = h.Write([]byte(value)) return fmt.Sprintf("%x", h.Sum(nil)) } - -func (p *Persister) hmacConstantCompare(ctx context.Context, value, hash string) bool { - for _, secret := range p.r.Config().SecretsSession(ctx) { - if subtle.ConstantTimeCompare([]byte(p.hmacValueWithSecret(ctx, value, secret)), []byte(hash)) == 1 { - return true - } - } - return false -} diff --git a/persistence/sql/persister_hmac_test.go b/persistence/sql/persister_hmac_test.go index 05dcd5985908..fa1d6e479308 100644 --- a/persistence/sql/persister_hmac_test.go +++ b/persistence/sql/persister_hmac_test.go @@ -8,9 +8,12 @@ import ( "os" "testing" - "github.com/ory/x/contextx" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" "github.com/ory/x/configx" + + "github.com/ory/x/contextx" + "github.com/ory/x/otelx" "github.com/gobuffalo/pop/v6" @@ -49,10 +52,10 @@ func (l *logRegistryOnly) Audit() *logrusx.Logger { panic("implement me") } -func (l *logRegistryOnly) Tracer(ctx context.Context) *otelx.Tracer { +func (l *logRegistryOnly) Tracer(context.Context) *otelx.Tracer { return otelx.NewNoop(l.l, new(otelx.Config)) } -func (l *logRegistryOnly) IdentityTraitsSchemas(ctx context.Context) (schema.Schemas, error) { +func (l *logRegistryOnly) IdentityTraitsSchemas(context.Context) (schema.IdentitySchemaList, error) { panic("implement me") } @@ -63,25 +66,36 @@ func (l *logRegistryOnly) IdentityValidator() *identity.Validator { var _ persisterDependencies = &logRegistryOnly{} func TestPersisterHMAC(t *testing.T) { + t.Parallel() + ctx := context.Background() - conf := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation()) - conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"foobarbaz"}) + baseSecret := "foobarbaz" + baseSecretBytes := []byte(baseSecret) + opts := []configx.OptionModifier{configx.SkipValidation(), configx.WithValue(config.ViperKeySecretsDefault, []string{baseSecret})} + conf := config.MustNew(t, logrusx.New("", ""), os.Stderr, &confighelpers.TestConfigProvider{Contextualizer: &contextx.Default{}, Options: opts}, opts...) c, err := pop.NewConnection(&pop.ConnectionDetails{URL: "sqlite://foo?mode=memory"}) require.NoError(t, err) - p, err := NewPersister(context.Background(), &logRegistryOnly{c: conf}, c) + p, err := NewPersister(ctx, &logRegistryOnly{c: conf}, c) require.NoError(t, err) - assert.True(t, p.hmacConstantCompare(context.Background(), "hashme", p.hmacValue(context.Background(), "hashme"))) - assert.False(t, p.hmacConstantCompare(context.Background(), "notme", p.hmacValue(context.Background(), "hashme"))) - assert.False(t, p.hmacConstantCompare(context.Background(), "hashme", p.hmacValue(context.Background(), "notme"))) - - hash := p.hmacValue(context.Background(), "hashme") - conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"notfoobarbaz"}) - assert.False(t, p.hmacConstantCompare(context.Background(), "hashme", hash)) - assert.True(t, p.hmacConstantCompare(context.Background(), "hashme", p.hmacValue(context.Background(), "hashme"))) - - conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"notfoobarbaz", "foobarbaz"}) - assert.True(t, p.hmacConstantCompare(context.Background(), "hashme", hash)) - assert.True(t, p.hmacConstantCompare(context.Background(), "hashme", p.hmacValue(context.Background(), "hashme"))) - assert.NotEqual(t, hash, p.hmacValue(context.Background(), "hashme")) + t.Run("case=behaves deterministically", func(t *testing.T) { + assert.Equal(t, hmacValueWithSecret(ctx, "hashme", baseSecretBytes), p.hmacValue(ctx, "hashme")) + assert.NotEqual(t, hmacValueWithSecret(ctx, "notme", baseSecretBytes), p.hmacValue(ctx, "hashme")) + assert.NotEqual(t, hmacValueWithSecret(ctx, "hashme", baseSecretBytes), p.hmacValue(ctx, "notme")) + }) + + hash := p.hmacValue(ctx, "hashme") + newSecret := "not" + baseSecret + + t.Run("case=with only new sectet", func(t *testing.T) { + ctx = confighelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{newSecret}) + assert.NotEqual(t, hmacValueWithSecret(ctx, "hashme", baseSecretBytes), p.hmacValue(ctx, "hashme")) + assert.Equal(t, hmacValueWithSecret(ctx, "hashme", []byte(newSecret)), p.hmacValue(ctx, "hashme")) + }) + + t.Run("case=with new and old secret", func(t *testing.T) { + ctx = confighelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{newSecret, baseSecret}) + assert.Equal(t, hmacValueWithSecret(ctx, "hashme", []byte(newSecret)), p.hmacValue(ctx, "hashme")) + assert.NotEqual(t, hash, p.hmacValue(ctx, "hashme")) + }) } diff --git a/persistence/sql/persister_recovery.go b/persistence/sql/persister_recovery.go index 468ba5a2b144..bb23d3fd319e 100644 --- a/persistence/sql/persister_recovery.go +++ b/persistence/sql/persister_recovery.go @@ -82,7 +82,7 @@ func (p *Persister) UseRecoveryToken(ctx context.Context, fID uuid.UUID, token s nid := p.NetworkID(ctx) if err := sqlcon.HandleError(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) { for _, secret := range p.r.Config().SecretsSession(ctx) { - if err = tx.Where("token = ? AND nid = ? AND NOT used AND selfservice_recovery_flow_id = ?", p.hmacValueWithSecret(ctx, token, secret), nid, fID).First(&rt); err != nil { + if err = tx.Where("token = ? AND nid = ? AND NOT used AND selfservice_recovery_flow_id = ?", hmacValueWithSecret(ctx, token, secret), nid, fID).First(&rt); err != nil { if !errors.Is(sqlcon.HandleError(err), sqlcon.ErrNoRows) { return err } diff --git a/persistence/sql/persister_session.go b/persistence/sql/persister_session.go index d53be8c52e8a..e0e14bcf5378 100644 --- a/persistence/sql/persister_session.go +++ b/persistence/sql/persister_session.go @@ -8,6 +8,10 @@ import ( "fmt" "time" + "github.com/ory/herodot" + "github.com/ory/x/dbal" + "github.com/ory/x/pointerx" + "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" "github.com/pkg/errors" @@ -16,6 +20,7 @@ import ( "github.com/ory/kratos/identity" "github.com/ory/kratos/session" + "github.com/ory/kratos/x" "github.com/ory/kratos/x/events" "github.com/ory/x/otelx" "github.com/ory/x/pagination/keysetpagination" @@ -78,6 +83,10 @@ func (p *Persister) ListSessions(ctx context.Context, active *bool, paginatorOpt paginatorOpts = append(paginatorOpts, keysetpagination.WithColumn("created_at", "DESC")) paginator := keysetpagination.GetPaginator(paginatorOpts...) + if _, err := uuid.FromString(paginator.Token().Parse("id")["id"]); err != nil { + return nil, 0, nil, errors.WithStack(x.PageTokenInvalid) + } + if err := p.Transaction(ctx, func(ctx context.Context, c *pop.Connection) error { q := c.Where("nid = ?", nid) if active != nil { @@ -176,6 +185,61 @@ func (p *Persister) ListSessionsByIdentity( return s, t, nil } +// ExtendSession updates the expiry of a session. +func (p *Persister) ExtendSession(ctx context.Context, sessionID uuid.UUID) (err error) { + ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.ExtendSession") + defer otelx.End(span, &err) + + nid := p.NetworkID(ctx) + s := new(session.Session) + var didRefresh bool + if err := errors.WithStack(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) { + lockBehavior := "" + if tx.Dialect.Name() == dbal.DriverCockroachDB { + // SKIP LOCKED returns no rows if the row is locked by another transaction. + lockBehavior = "FOR UPDATE SKIP LOCKED" + } + + if err := tx. + Where( + // We make use of the fact that CRDB supports FOR UPDATE as part of the WHERE clause. + fmt.Sprintf("id = ? AND nid = ? %s", lockBehavior), + sessionID, nid, + ).First(s); err != nil { + + // This is a special case for CockroachDB. If the row is locked, we do not see the session. Therefor we return + // a 404 not found error indicating to the user that the session might already be updated by someone else. + if errors.Is(err, sqlcon.ErrNoRows) && tx.Dialect.Name() == dbal.DriverCockroachDB { + return errors.WithStack(herodot.ErrNotFound.WithReason("The session you are trying to extend is already being extended by another request or does not exist.")) + } + + return sqlcon.HandleError(err) + } + + if !s.CanBeRefreshed(ctx, p.r.Config()) { + // This prevents excessive writes to the database. + return nil + } + + didRefresh = true + s = s.Refresh(ctx, p.r.Config()) + + if _, err := tx.Where("id = ? AND nid = ?", sessionID, nid).UpdateQuery(s, "expires_at"); err != nil { + return sqlcon.HandleError(err) + } + + return nil + })); err != nil { + return err + } + + if didRefresh { + trace.SpanFromContext(ctx).AddEvent(events.NewSessionLifespanExtended(ctx, s.ID, s.IdentityID, s.ExpiresAt)) + } + + return nil +} + // UpsertSession creates a session if not found else updates. // This operation also inserts Session device records when a session is being created. // The update operation skips updating Session device records since only one record would need to be updated in this case. @@ -185,10 +249,22 @@ func (p *Persister) UpsertSession(ctx context.Context, s *session.Session) (err s.NID = p.NetworkID(ctx) - return errors.WithStack(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) error { + var updated bool + defer func() { + if err != nil { + return + } + if updated { + trace.SpanFromContext(ctx).AddEvent(events.NewSessionChanged(ctx, string(s.AuthenticatorAssuranceLevel), s.ID, s.IdentityID)) + } else { + trace.SpanFromContext(ctx).AddEvent(events.NewSessionIssued(ctx, string(s.AuthenticatorAssuranceLevel), s.ID, s.IdentityID)) + } + }() + + return errors.WithStack(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) { + updated = false exists := false if !s.ID.IsNil() { - var err error exists, err = tx.Where("id = ? AND nid = ?", s.ID, s.NID).Exists(new(session.Session)) if err != nil { return sqlcon.HandleError(err) @@ -198,10 +274,10 @@ func (p *Persister) UpsertSession(ctx context.Context, s *session.Session) (err if exists { // This must not be eager or identities will be created / updated // Only update session and not corresponding session device records - if err := tx.Update(s); err != nil { + if err := tx.Update(s, "issued_at", "identity_id", "nid"); err != nil { return sqlcon.HandleError(err) } - trace.SpanFromContext(ctx).AddEvent(events.NewSessionChanged(ctx, string(s.AuthenticatorAssuranceLevel), s.ID, s.IdentityID)) + updated = true return nil } @@ -216,10 +292,10 @@ func (p *Persister) UpsertSession(ctx context.Context, s *session.Session) (err device.NID = s.NID if device.Location != nil { - device.Location = stringsx.GetPointer(stringsx.TruncateByteLen(*device.Location, SessionDeviceLocationMaxLength)) + device.Location = pointerx.Ptr(stringsx.TruncateByteLen(*device.Location, SessionDeviceLocationMaxLength)) } if device.UserAgent != nil { - device.UserAgent = stringsx.GetPointer(stringsx.TruncateByteLen(*device.UserAgent, SessionDeviceUserAgentMaxLength)) + device.UserAgent = pointerx.Ptr(stringsx.TruncateByteLen(*device.UserAgent, SessionDeviceUserAgentMaxLength)) } if err := p.DevicePersister.CreateDevice(ctx, device); err != nil { @@ -227,7 +303,6 @@ func (p *Persister) UpsertSession(ctx context.Context, s *session.Session) (err } } - trace.SpanFromContext(ctx).AddEvent(events.NewSessionIssued(ctx, string(s.AuthenticatorAssuranceLevel), s.ID, s.IdentityID)) return nil })) } diff --git a/persistence/sql/persister_test.go b/persistence/sql/persister_test.go index 7b21c2b63019..94b6d50cac3c 100644 --- a/persistence/sql/persister_test.go +++ b/persistence/sql/persister_test.go @@ -29,6 +29,7 @@ import ( "github.com/ory/kratos/internal" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence/sql" + "github.com/ory/kratos/persistence/sql/batch" sqltesthelpers "github.com/ory/kratos/persistence/sql/testhelpers" "github.com/ory/kratos/schema" errorx "github.com/ory/kratos/selfservice/errorx/test" @@ -94,18 +95,22 @@ func pl(t testing.TB) func(lvl logging.Level, s string, args ...interface{}) { func createCleanDatabases(t testing.TB) map[string]*driver.RegistryDefault { conns := map[string]string{ - "sqlite": "sqlite://file:" + t.TempDir() + "/db.sqlite?_fk=true", + "sqlite": "sqlite://file:" + t.TempDir() + "/db.sqlite?_fk=true&max_conns=1&lock=false", } var l sync.Mutex if !testing.Short() { funcs := map[string]func(t testing.TB) string{ // fandom-start - //"postgres": dockertest.RunTestPostgreSQL, + //"postgres": func(t testing.TB) string { + // return dockertest.RunTestPostgreSQLWithVersion(t, "16") + //}, // fandom-end - "mysql": dockertest.RunTestMySQL, + "mysql": func(t testing.TB) string { + return dockertest.RunTestMySQLWithVersion(t, "8.0") + }, // fandom-start - //"cockroach": dockertest.NewLocalTestCRDBServer + //"cockroach": newLocalTestCRDBServer, // fandom-end } @@ -164,63 +169,56 @@ func createCleanDatabases(t testing.TB) map[string]*driver.RegistryDefault { } func TestPersister(t *testing.T) { + t.Parallel() + conns := createCleanDatabases(t) - ctx := context.Background() + ctx := testhelpers.WithDefaultIdentitySchema(context.Background(), "file://./stub/identity.schema.json") - for name := range conns { - name := name - reg := conns[name] + for name, reg := range conns { t.Run(fmt.Sprintf("database=%s", name), func(t *testing.T) { t.Parallel() _, p := testhelpers.NewNetwork(t, ctx, reg.Persister()) - conf := reg.Config() - t.Logf("DSN: %s", conf.DSN(ctx)) + t.Logf("DSN: %s", reg.Config().DSN(ctx)) - // This test must remain the first test in the test suite! t.Run("racy identity creation", func(t *testing.T) { - defaultSchema := schema.Schema{ - ID: config.DefaultIdentityTraitsSchemaID, - URL: urlx.ParseOrPanic("file://./stub/identity.schema.json"), - RawURL: "file://./stub/identity.schema.json", - } + t.Parallel() var wg sync.WaitGroup - testhelpers.SetDefaultIdentitySchema(reg.Config(), defaultSchema.RawURL) + _, ps := testhelpers.NewNetwork(t, ctx, reg.Persister()) - for i := 0; i < 10; i++ { + for i := range 10 { wg.Add(1) - // capture i - ii := i go func() { defer wg.Done() id := ri.NewIdentity("") id.SetCredentials(ri.CredentialsTypePassword, ri.Credentials{ Type: ri.CredentialsTypePassword, - Identifiers: []string{fmt.Sprintf("racy identity %d", ii)}, + Identifiers: []string{fmt.Sprintf("racy identity %d", i)}, Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`), }) id.Traits = ri.Traits("{}") - require.NoError(t, ps.CreateIdentity(context.Background(), id)) + require.NoError(t, ps.CreateIdentity(ctx, id)) }() } wg.Wait() }) - t.Run("case=credentials types", func(t *testing.T) { + t.Run("case=credential types exist", func(t *testing.T) { + t.Parallel() for _, ct := range []ri.CredentialsType{ri.CredentialsTypeOIDC, ri.CredentialsTypePassword} { require.NoError(t, p.(*sql.Persister).Connection(context.Background()).Where("name = ?", ct).First(&ri.CredentialsTypeTable{})) } }) t.Run("contract=identity.TestPool", func(t *testing.T) { - pop.SetLogger(pl(t)) - identity.TestPool(ctx, conf, p, reg.IdentityManager(), name)(t) + t.Parallel() + identity.TestPool(ctx, p, reg.IdentityManager(), name)(t) }) t.Run("contract=identity.TestPool (case sensitive)", func(t *testing.T) { pop.SetLogger(pl(t)) @@ -230,54 +228,58 @@ func TestPersister(t *testing.T) { identity.TestPool(ctx, caseConf, p, reg.IdentityManager(), name)(t) }) t.Run("contract=registration.TestFlowPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) + t.Parallel() registration.TestFlowPersister(ctx, p)(t) }) t.Run("contract=errorx.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) + t.Parallel() errorx.TestPersister(ctx, p)(t) }) t.Run("contract=login.TestFlowPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) + t.Parallel() login.TestFlowPersister(ctx, p)(t) }) t.Run("contract=settings.TestFlowPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - settings.TestFlowPersister(ctx, conf, p)(t) + t.Parallel() + settings.TestFlowPersister(ctx, p)(t) }) t.Run("contract=session.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - session.TestPersister(ctx, conf, p)(t) + t.Parallel() + session.TestPersister(ctx, reg.Config(), p)(t) }) t.Run("contract=sessiontokenexchange.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - sessiontokenexchange.TestPersister(ctx, conf, p)(t) + t.Parallel() + sessiontokenexchange.TestPersister(ctx, p)(t) }) t.Run("contract=courier.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) + t.Parallel() upsert, insert := sqltesthelpers.DefaultNetworkWrapper(p) courier.TestPersister(ctx, upsert, insert)(t) }) t.Run("contract=verification.TestFlowPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - verification.TestFlowPersister(ctx, conf, p)(t) + t.Parallel() + verification.TestFlowPersister(ctx, p)(t) }) t.Run("contract=recovery.TestFlowPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - recovery.TestFlowPersister(ctx, conf, p)(t) + t.Parallel() + recovery.TestFlowPersister(ctx, p)(t) }) t.Run("contract=link.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - link.TestPersister(ctx, conf, p)(t) + t.Parallel() + link.TestPersister(ctx, p)(t) }) t.Run("contract=code.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) - code.TestPersister(ctx, conf, p)(t) + t.Parallel() + code.TestPersister(ctx, p)(t) }) t.Run("contract=continuity.TestPersister", func(t *testing.T) { - pop.SetLogger(pl(t)) + t.Parallel() continuity.TestPersister(ctx, p)(t) }) + t.Run("contract=batch.TestPersister", func(t *testing.T) { + t.Parallel() + batch.TestPersister(ctx, reg.Tracer(ctx), p)(t) + }) }) } } @@ -294,6 +296,8 @@ func getErr(args ...interface{}) error { } func TestPersister_Transaction(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) p := reg.Persister() diff --git a/persistence/sql/persister_verification.go b/persistence/sql/persister_verification.go index 8d983ed1635d..7feae0592ae7 100644 --- a/persistence/sql/persister_verification.go +++ b/persistence/sql/persister_verification.go @@ -82,7 +82,7 @@ func (p *Persister) UseVerificationToken(ctx context.Context, fID uuid.UUID, tok nid := p.NetworkID(ctx) if err := sqlcon.HandleError(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) { for _, secret := range p.r.Config().SecretsSession(ctx) { - if err = tx.Where("token = ? AND nid = ? AND NOT used AND selfservice_verification_flow_id = ?", p.hmacValueWithSecret(ctx, token, secret), nid, fID).First(&rt); err != nil { + if err = tx.Where("token = ? AND nid = ? AND NOT used AND selfservice_verification_flow_id = ?", hmacValueWithSecret(ctx, token, secret), nid, fID).First(&rt); err != nil { if !errors.Is(sqlcon.HandleError(err), sqlcon.ErrNoRows) { return err } diff --git a/proto/oidc/v1/state.proto b/proto/oidc/v1/state.proto new file mode 100644 index 000000000000..255f7f118e05 --- /dev/null +++ b/proto/oidc/v1/state.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package oidc.v1; + +message State { + bytes flow_id = 1; + bytes session_token_exchange_code_sha512 = 2; + string provider_id = 3; + string pkce_verifier = 4; +} diff --git a/quickstart-mysql.yml b/quickstart-mysql.yml index 7412e22c55df..f0cda5c4ea69 100644 --- a/quickstart-mysql.yml +++ b/quickstart-mysql.yml @@ -1,4 +1,4 @@ -version: '3.7' +version: "3.7" services: kratos-migrate: @@ -10,7 +10,7 @@ services: - DSN=mysql://root:secret@tcp(mysqld:3306)/mysql?max_conns=20&max_idle_conns=4 mysqld: - image: mysql:5.7 + image: mysql:8.0 ports: - "3306:3306" environment: diff --git a/quickstart-postgres.yml b/quickstart-postgres.yml index 8c0dee47a2f3..93cad3a2ffc5 100644 --- a/quickstart-postgres.yml +++ b/quickstart-postgres.yml @@ -1,4 +1,4 @@ -version: '3.7' +version: "3.7" services: kratos-migrate: @@ -10,7 +10,7 @@ services: - DSN=postgres://kratos:secret@postgresd:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 postgresd: - image: postgres:11.8 + image: postgres:14 ports: - "5432:5432" environment: diff --git a/quickstart.yml b/quickstart.yml index 1af5775bdd4c..10a331c397b1 100644 --- a/quickstart.yml +++ b/quickstart.yml @@ -1,7 +1,7 @@ version: '3.7' services: kratos-migrate: - image: oryd/kratos:v1.0.0 + image: oryd/kratos:v1.2.0 environment: - DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true&mode=rwc volumes: @@ -17,17 +17,20 @@ services: networks: - intranet kratos-selfservice-ui-node: - image: oryd/kratos-selfservice-ui-node:v1.0.0 + image: oryd/kratos-selfservice-ui-node:v1.2.0 environment: - KRATOS_PUBLIC_URL=http://kratos:4433/ - KRATOS_BROWSER_URL=http://127.0.0.1:4433/ + - COOKIE_SECRET=changeme + - CSRF_COOKIE_NAME=ory_csrf_ui + - CSRF_COOKIE_SECRET=changeme networks: - intranet restart: on-failure kratos: depends_on: - kratos-migrate - image: oryd/kratos:v1.0.0 + image: oryd/kratos:v1.2.0 ports: - '4433:4433' # public - '4434:4434' # admin diff --git a/schema/errors.go b/schema/errors.go index 30c1f72976f3..6ff52a5047c2 100644 --- a/schema/errors.go +++ b/schema/errors.go @@ -117,12 +117,21 @@ func NewInvalidCredentialsError() error { ValidationError: &jsonschema.ValidationError{ Message: `the provided credentials are invalid, check for spelling mistakes in your password or username, email address, or phone number`, InstancePtr: "#/", - Context: &ValidationErrorContextPasswordPolicyViolation{}, }, Messages: new(text.Messages).Add(text.NewErrorValidationInvalidCredentials()), }) } +func NewAccountNotFoundError() error { + return errors.WithStack(&ValidationError{ + ValidationError: &jsonschema.ValidationError{ + Message: "this account does not exist or has no login method configured", + InstancePtr: "#/identifier", + }, + Messages: new(text.Messages).Add(text.NewErrorValidationAccountNotFound()), + }) +} + type ValidationErrorContextDuplicateCredentialsError struct { AvailableCredentials []string `json:"available_credential_types"` AvailableOIDCProviders []string `json:"available_oidc_providers"` diff --git a/schema/extension.go b/schema/extension.go index c217f409c8c3..62db865d1f3f 100644 --- a/schema/extension.go +++ b/schema/extension.go @@ -12,10 +12,11 @@ import ( "github.com/ory/jsonschema/v3" "github.com/ory/kratos/embedx" + "github.com/ory/x/jsonschemax" ) const ( - extensionName string = "ory.sh/kratos" + ExtensionName string = "ory.sh/kratos" ) type ( @@ -27,6 +28,9 @@ type ( WebAuthn struct { Identifier bool `json:"identifier"` } `json:"webauthn"` + Passkey struct { + DisplayName bool `json:"display_name"` + } `json:"passkey"` TOTP struct { AccountName bool `json:"account_name"` } `json:"totp"` @@ -64,6 +68,16 @@ type ( ExtensionRunnerOption func(*ExtensionRunner) ) +func (e *ExtensionConfig) EnhancePath(path jsonschemax.Path) map[string]any { + props := path.CustomProperties + if props == nil { + props = make(map[string]any) + } + props[ExtensionName] = e + + return props +} + func WithValidateRunners(runners ...ValidateExtension) ExtensionRunnerOption { return func(r *ExtensionRunner) { r.validateRunners = append(r.validateRunners, runners...) @@ -91,7 +105,7 @@ func NewExtensionRunner(ctx context.Context, opts ...ExtensionRunnerOption) (*Ex } r.compile = func(ctx jsonschema.CompilerContext, m map[string]interface{}) (interface{}, error) { - if raw, ok := m[extensionName]; ok { + if raw, ok := m[ExtensionName]; ok { var b bytes.Buffer if err := json.NewEncoder(&b).Encode(raw); err != nil { return nil, errors.WithStack(err) @@ -136,7 +150,7 @@ func NewExtensionRunner(ctx context.Context, opts ...ExtensionRunnerOption) (*Ex } func (r *ExtensionRunner) Register(compiler *jsonschema.Compiler) *ExtensionRunner { - compiler.Extensions[extensionName] = r.Extension() + compiler.Extensions[ExtensionName] = r.Extension() return r } diff --git a/schema/handler.go b/schema/handler.go index 3cbcc529417e..acf6a0dc786a 100644 --- a/schema/handler.go +++ b/schema/handler.go @@ -13,25 +13,21 @@ import ( "os" "strings" - "github.com/ory/x/otelx" - - "github.com/ory/x/pagination/migrationpagination" - - "github.com/ory/kratos/driver/config" - "github.com/julienschmidt/httprouter" "github.com/pkg/errors" "github.com/ory/herodot" - + "github.com/ory/kratos/driver/config" "github.com/ory/kratos/x" + "github.com/ory/x/otelx" + "github.com/ory/x/pagination/migrationpagination" ) type ( handlerDependencies interface { x.WriterProvider x.LoggingProvider - IdentityTraitsProvider + IdentitySchemaProvider x.CSRFProvider config.Provider x.TracingProvider @@ -73,7 +69,16 @@ func (h *Handler) RegisterAdminRoutes(admin *x.RouterAdmin) { // //nolint:deadcode,unused //lint:ignore U1000 Used to generate Swagger and OpenAPI definitions -type identitySchema = json.RawMessage +type identitySchema json.RawMessage + +func (m identitySchema) MarshalJSON() ([]byte, error) { + return json.RawMessage(m).MarshalJSON() +} + +func (m *identitySchema) UnmarshalJSON(data []byte) error { + mm := json.RawMessage(*m) + return mm.UnmarshalJSON(data) +} // Get Identity JSON Schema Response // @@ -155,7 +160,7 @@ type identitySchemaContainer struct { // The ID of the Identity JSON Schema ID string `json:"id"` // The actual Identity JSON Schema - Schema identitySchema `json:"schema"` + Schema json.RawMessage `json:"schema"` } // List Identity JSON Schemas Response @@ -218,7 +223,7 @@ func (h *Handler) getAll(w http.ResponseWriter, r *http.Request, ps httprouter.P return } - raw, err := io.ReadAll(src) + raw, err := io.ReadAll(io.LimitReader(src, 1024*1024)) _ = src.Close() if err != nil { h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The file for this JSON Schema ID could not be found or opened. This is a configuration issue.").WithDebugf("%+v", err))) diff --git a/schema/handler_test.go b/schema/handler_test.go index a5bdf06a5a83..36aeb3aea75d 100644 --- a/schema/handler_test.go +++ b/schema/handler_test.go @@ -15,13 +15,11 @@ import ( "strings" "testing" - "github.com/ory/client-go" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/ory/client-go" _ "github.com/ory/jsonschema/v3/fileloader" - "github.com/ory/kratos/driver/config" "github.com/ory/kratos/internal" "github.com/ory/kratos/schema" @@ -191,7 +189,7 @@ func TestHandler(t *testing.T) { body := getFromTSPaginated(t, 0, 2, http.StatusOK) var result []client.IdentitySchemaContainer - require.NoError(t, json.Unmarshal(body, &result)) + require.NoError(t, json.Unmarshal(body, &result), "%s", body) ids_orig := []string{} for _, s := range schemas { diff --git a/schema/schema.go b/schema/schema.go index 305064edf9e3..40671c94a000 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -4,6 +4,7 @@ package schema import ( + "cmp" "context" "encoding/base64" "io" @@ -21,16 +22,58 @@ import ( "github.com/ory/x/urlx" ) +var _ IdentitySchemaList = (*Schemas)(nil) + type Schemas []Schema -type IdentityTraitsProvider interface { - IdentityTraitsSchemas(ctx context.Context) (Schemas, error) + +type IdentitySchemaProvider interface { + IdentityTraitsSchemas(ctx context.Context) (IdentitySchemaList, error) } -func (s Schemas) GetByID(id string) (*Schema, error) { - if id == "" { - id = config.DefaultIdentityTraitsSchemaID +type deps interface { + config.Provider +} + +type DefaultIdentitySchemaProvider struct { + d deps +} + +func NewDefaultIdentityTraitsProvider(d deps) *DefaultIdentitySchemaProvider { + return &DefaultIdentitySchemaProvider{d: d} +} + +func (d *DefaultIdentitySchemaProvider) IdentityTraitsSchemas(ctx context.Context) (IdentitySchemaList, error) { + ms, err := d.d.Config().IdentityTraitsSchemas(ctx) + if err != nil { + return nil, err + } + + var ss Schemas + for _, s := range ms { + surl, err := url.Parse(s.URL) + if err != nil { + return nil, errors.WithStack(err) + } + + ss = append(ss, Schema{ + ID: s.ID, + URL: surl, + RawURL: s.URL, + }) } + return ss, nil +} + +type IdentitySchemaList interface { + GetByID(id string) (*Schema, error) + Total() int + List(page, perPage int) Schemas +} + +func (s Schemas) GetByID(id string) (*Schema, error) { + id = cmp.Or(id, config.DefaultIdentityTraitsSchemaID) + for _, ss := range s { if ss.ID == id { return &ss, nil @@ -83,7 +126,7 @@ func GetKeysInOrder(ctx context.Context, schemaRef string) ([]string, error) { if err != nil { return nil, errors.WithStack(err) } - schema, err := io.ReadAll(sio) + schema, err := io.ReadAll(io.LimitReader(sio, 1024*1024)) if err != nil { return nil, errors.WithStack(err) } @@ -98,11 +141,16 @@ func GetKeysInOrder(ctx context.Context, schemaRef string) ([]string, error) { } type Schema struct { - ID string `json:"id"` - URL *url.URL `json:"-"` - RawURL string `json:"url"` + ID string `json:"id"` + URL *url.URL `json:"-"` + // RawURL contains the raw URL value as it was passed in the configuration. URL parsing can break base64 encoded URLs. + RawURL string `json:"url"` } func (s *Schema) SchemaURL(host *url.URL) *url.URL { - return urlx.AppendPaths(host, SchemasPath, base64.RawURLEncoding.EncodeToString([]byte(s.ID))) + return IDToURL(host, s.ID) +} + +func IDToURL(host *url.URL, id string) *url.URL { + return urlx.AppendPaths(host, SchemasPath, base64.RawURLEncoding.EncodeToString([]byte(id))) } diff --git a/script/testenv.sh b/script/testenv.sh index 15977c10fc8b..becd47b98c7a 100755 --- a/script/testenv.sh +++ b/script/testenv.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash docker rm -f kratos_test_database_mysql kratos_test_database_postgres kratos_test_database_cockroach kratos_test_hydra || true -docker run --platform linux/amd64 --name kratos_test_database_mysql -p 3444:3306 -e MYSQL_ROOT_PASSWORD=secret -d mysql:8.0.34 -docker run --platform linux/amd64 --name kratos_test_database_postgres -p 3445:5432 -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=postgres -d postgres:11.8 postgres -c log_statement=all -docker run --platform linux/amd64 --name kratos_test_database_cockroach -p 3446:26257 -p 3447:8080 -d cockroachdb/cockroach:v22.2.6 start-single-node --insecure -docker run --platform linux/amd64 --name kratos_test_hydra -p 4444:4444 -p 4445:4445 -d -e DSN=memory -e URLS_SELF_ISSUER=http://localhost:4444/ -e URLS_LOGIN=http://localhost:4446/login -e URLS_CONSENT=http://localhost:4446/consent oryd/hydra:v2.0.2 serve all --dev +docker run --name kratos_test_database_mysql -p 3444:3306 -e MYSQL_ROOT_PASSWORD=secret -d mysql:8.0 +docker run --name kratos_test_database_postgres -p 3445:5432 -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=postgres -d postgres:14 postgres -c log_statement=all +docker run --name kratos_test_database_cockroach -p 3446:26257 -p 3447:8080 -d cockroachdb/cockroach:v22.2.6 start-single-node --insecure +docker run --name kratos_test_hydra -p 4444:4444 -p 4445:4445 -d -e DSN=memory -e URLS_SELF_ISSUER=http://localhost:4444/ -e URLS_LOGIN=http://localhost:4446/login -e URLS_CONSENT=http://localhost:4446/consent oryd/hydra:v2.0.2 serve all --dev source script/test-envs.sh diff --git a/selfservice/flow/continue_with.go b/selfservice/flow/continue_with.go index 7a5f9ce22410..bac63d72a273 100644 --- a/selfservice/flow/continue_with.go +++ b/selfservice/flow/continue_with.go @@ -89,6 +89,8 @@ type ContinueWithVerificationUIFlow struct { // The URL of the verification flow // + // If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. + // // required: false URL string `json:"url,omitempty"` } @@ -134,6 +136,7 @@ type ContinueWithSettingsUI struct { // // required: true Action ContinueWithActionShowSettingsUI `json:"action"` + // Flow contains the ID of the verification flow // // required: true @@ -146,13 +149,21 @@ type ContinueWithSettingsUIFlow struct { // // required: true ID uuid.UUID `json:"id"` + + // The URL of the settings flow + // + // If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. + // + // required: false + URL string `json:"url,omitempty"` } -func NewContinueWithSettingsUI(f Flow) *ContinueWithSettingsUI { +func NewContinueWithSettingsUI(f Flow, redirectTo string) *ContinueWithSettingsUI { return &ContinueWithSettingsUI{ Action: ContinueWithActionShowSettingsUIString, Flow: ContinueWithSettingsUIFlow{ - ID: f.GetID(), + ID: f.GetID(), + URL: redirectTo, }, } } @@ -188,6 +199,8 @@ type ContinueWithRecoveryUIFlow struct { // The URL of the recovery flow // + // If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows. + // // required: false URL string `json:"url,omitempty"` } @@ -201,6 +214,36 @@ func NewContinueWithRecoveryUI(f Flow) *ContinueWithRecoveryUI { } } +// swagger:enum ContinueWithActionRedirectBrowserTo +type ContinueWithActionRedirectBrowserTo string + +// #nosec G101 -- only a key constant +const ( + ContinueWithActionRedirectBrowserToString ContinueWithActionRedirectBrowserTo = "redirect_browser_to" +) + +// Indicates, that the UI flow could be continued by showing a recovery ui +// +// swagger:model continueWithRedirectBrowserTo +type ContinueWithRedirectBrowserTo struct { + // Action will always be `redirect_browser_to` + // + // required: true + Action ContinueWithActionRedirectBrowserTo `json:"action"` + + // The URL to redirect the browser to + // + // required: true + RedirectTo string `json:"redirect_browser_to"` +} + +func NewContinueWithRedirectBrowserTo(redirectTo string) *ContinueWithRedirectBrowserTo { + return &ContinueWithRedirectBrowserTo{ + Action: ContinueWithActionRedirectBrowserToString, + RedirectTo: redirectTo, + } +} + func ErrorWithContinueWith(err *herodot.DefaultError, continueWith ...ContinueWith) *herodot.DefaultError { if err.DetailsField == nil { err.DetailsField = map[string]interface{}{} diff --git a/selfservice/flow/error.go b/selfservice/flow/error.go index d666822fa5c0..8449ec58ad3e 100644 --- a/selfservice/flow/error.go +++ b/selfservice/flow/error.go @@ -73,7 +73,7 @@ func NewFlowReplacedError(message *text.Message) *ReplacedError { return &ReplacedError{ DefaultError: x.ErrGone.WithID(text.ErrIDSelfServiceFlowReplaced). WithError("self-service flow replaced"). - WithReasonf(message.Text), + WithReason(message.Text), } } diff --git a/selfservice/flow/error_test.go b/selfservice/flow/error_test.go index 98b1ad32e9c4..3bf62d6091e2 100644 --- a/selfservice/flow/error_test.go +++ b/selfservice/flow/error_test.go @@ -97,6 +97,10 @@ func (t *testFlow) SetState(state State) { t.State = state } +func (t *testFlow) GetTransientPayload() json.RawMessage { + return nil +} + func newTestFlow(r *http.Request, flowType Type) Flow { id := x.NewUUID() requestURL := x.RequestURL(r).String() diff --git a/selfservice/flow/flow.go b/selfservice/flow/flow.go index ee9fbba6638d..d5ad7b740a97 100644 --- a/selfservice/flow/flow.go +++ b/selfservice/flow/flow.go @@ -5,6 +5,7 @@ package flow import ( "context" + "encoding/json" "net/http" "net/url" @@ -39,6 +40,7 @@ type Flow interface { GetState() State SetState(State) GetFlowName() FlowName + GetTransientPayload() json.RawMessage } type FlowWithRedirect interface { diff --git a/selfservice/flow/login/error.go b/selfservice/flow/login/error.go index 94311b92a815..e7da5da2ca81 100644 --- a/selfservice/flow/login/error.go +++ b/selfservice/flow/login/error.go @@ -79,10 +79,12 @@ func (s *ErrorHandler) PrepareReplacementForExpiredFlow(w http.ResponseWriter, r } func (s *ErrorHandler) WriteFlowError(w http.ResponseWriter, r *http.Request, f *Flow, group node.UiNodeGroup, err error) { - s.d.Audit(). + logger := s.d.Audit(). WithError(err). WithRequest(r). - WithField("login_flow", f). + WithField("login_flow", f.ToLoggerField()) + + logger. Info("Encountered self-service login error.") if f == nil { diff --git a/selfservice/flow/login/error_test.go b/selfservice/flow/login/error_test.go index 5cc78c35bda1..20481cf290f9 100644 --- a/selfservice/flow/login/error_test.go +++ b/selfservice/flow/login/error_test.go @@ -74,7 +74,12 @@ func TestHandleError(t *testing.T) { require.NoError(t, err) for _, s := range reg.LoginStrategies(context.Background()) { - require.NoError(t, s.PopulateLoginMethod(req, identity.AuthenticatorAssuranceLevel1, f)) + switch s.(type) { + case login.UnifiedFormHydrator: + require.NoError(t, s.(login.UnifiedFormHydrator).PopulateLoginMethod(req, identity.AuthenticatorAssuranceLevel1, f)) + case login.FormHydrator: + require.NoError(t, s.(login.FormHydrator).PopulateLoginMethodFirstFactor(req, f)) + } } require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), f)) @@ -87,7 +92,7 @@ func TestHandleError(t *testing.T) { defer res.Body.Close() require.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()+"?id=") - sse, _, err := sdk.FrontendApi.GetFlowError(context.Background()).Id(res.Request.URL.Query().Get("id")).Execute() + sse, _, err := sdk.FrontendAPI.GetFlowError(context.Background()).Id(res.Request.URL.Query().Get("id")).Execute() require.NoError(t, err) return sse.Error, nil diff --git a/selfservice/flow/login/extension_identifier_label.go b/selfservice/flow/login/extension_identifier_label.go index 9d28dd8ecf9f..961a7c5d7324 100644 --- a/selfservice/flow/login/extension_identifier_label.go +++ b/selfservice/flow/login/extension_identifier_label.go @@ -67,6 +67,7 @@ func GetIdentifierLabelFromSchemaWithField(ctx context.Context, schemaURL string func (i *identifierLabelExtension) Run(_ jsonschema.CompilerContext, config schema.ExtensionConfig, rawSchema map[string]interface{}) error { if config.Credentials.Password.Identifier || config.Credentials.WebAuthn.Identifier || + config.Credentials.Passkey.DisplayName || config.Credentials.TOTP.AccountName || config.Credentials.Code.Identifier { if title, ok := rawSchema["title"]; ok { diff --git a/selfservice/flow/login/extension_identifier_label_test.go b/selfservice/flow/login/extension_identifier_label_test.go index 3b3a21400fb1..9d2bbc80e667 100644 --- a/selfservice/flow/login/extension_identifier_label_test.go +++ b/selfservice/flow/login/extension_identifier_label_test.go @@ -102,6 +102,13 @@ func TestGetIdentifierLabelFromSchema(t *testing.T) { }, expected: text.NewInfoNodeLabelGenerated("Email"), }, + { + name: "email for passkey", + emailConfig: func(c *schema.ExtensionConfig) { + c.Credentials.Passkey.DisplayName = true + }, + expected: text.NewInfoNodeLabelGenerated("Email"), + }, { name: "email for all", emailConfig: func(c *schema.ExtensionConfig) { diff --git a/selfservice/flow/login/flow.go b/selfservice/flow/login/flow.go index 5dd8422cdb84..1c04dcaf2ef4 100644 --- a/selfservice/flow/login/flow.go +++ b/selfservice/flow/login/flow.go @@ -9,6 +9,7 @@ import ( "fmt" "net/http" "net/url" + "strconv" "strings" "time" @@ -140,6 +141,22 @@ type Flow struct { // Only used internally RawIDTokenNonce string `json:"-" db:"-"` + + // TransientPayload is used to pass data from the login to hooks and email templates + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" faker:"-" db:"-"` + + // Contains a list of actions, that could follow this flow + // + // It can, for example, contain a reference to the verification flow, created as part of the user's + // registration. + ContinueWithItems []flow.ContinueWith `json:"-" db:"-" faker:"-" ` + + // ReturnToVerification contains the redirect URL for the verification flow. + ReturnToVerification string `json:"-" db:"-"` + + isAccountLinkingFlow bool `json:"-" db:"-"` } var _ flow.Flow = new(Flow) @@ -165,6 +182,8 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques return nil, err } + refresh, _ := strconv.ParseBool(r.URL.Query().Get("refresh")) + return &Flow{ ID: id, OAuth2LoginChallenge: hydraLoginChallenge, @@ -177,7 +196,7 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques RequestURL: requestURL, CSRFToken: csrf, Type: flowType, - Refresh: r.URL.Query().Get("refresh") == "true", + Refresh: refresh, RequestedAAL: identity.AuthenticatorAssuranceLevel(strings.ToLower(stringsx.Coalesce( r.URL.Query().Get("aal"), string(identity.AuthenticatorAssuranceLevel1)))), @@ -213,7 +232,9 @@ func (f Flow) GetID() uuid.UUID { return f.ID } -func (f *Flow) IsForced() bool { +// IsRefresh returns true if the login flow was triggered to re-authenticate the user. +// This is the case if the refresh query parameter is set to true. +func (f *Flow) IsRefresh() bool { return f.Refresh } @@ -290,3 +311,38 @@ func (f *Flow) GetFlowName() flow.FlowName { func (f *Flow) SetState(state flow.State) { f.State = State(state) } + +func (t *Flow) GetTransientPayload() json.RawMessage { + return t.TransientPayload +} + +var _ flow.FlowWithContinueWith = new(Flow) + +func (f *Flow) AddContinueWith(c flow.ContinueWith) { + f.ContinueWithItems = append(f.ContinueWithItems, c) +} + +func (f *Flow) ContinueWith() []flow.ContinueWith { + return f.ContinueWithItems +} + +func (f *Flow) SetReturnToVerification(to string) { + f.ReturnToVerification = to +} + +func (f *Flow) ToLoggerField() map[string]interface{} { + if f == nil { + return map[string]interface{}{} + } + return map[string]interface{}{ + "id": f.ID.String(), + "return_to": f.ReturnTo, + "request_url": f.RequestURL, + "active": f.Active, + "type": f.Type, + "nid": f.NID, + "state": f.State, + "refresh": f.Refresh, + "requested_aal": f.RequestedAAL, + } +} diff --git a/selfservice/flow/login/flow_test.go b/selfservice/flow/login/flow_test.go index 685d2604c43d..24c9d03dcb52 100644 --- a/selfservice/flow/login/flow_test.go +++ b/selfservice/flow/login/flow_test.go @@ -65,6 +65,24 @@ func TestNewFlow(t *testing.T) { assert.Equal(t, identity.AuthenticatorAssuranceLevel1, r.RequestedAAL) }) + t.Run("type=refresh", func(t *testing.T) { + t.Run("case=refresh accepts any truthy value", func(t *testing.T) { + parameters := []string{"true", "True", "1"} + + for _, refresh := range parameters { + r, err := login.NewFlow(conf, 0, "csrf", &http.Request{URL: &url.URL{Path: "/", RawQuery: fmt.Sprintf("refresh=%v", refresh)}, Host: "ory.sh"}, flow.TypeBrowser) + require.NoError(t, err) + assert.True(t, r.Refresh) + } + }) + + t.Run("case=refresh silently ignores invalid values", func(t *testing.T) { + r, err := login.NewFlow(conf, 0, "csrf", &http.Request{URL: &url.URL{Path: "/", RawQuery: "refresh=foo"}, Host: "ory.sh"}, flow.TypeBrowser) + require.NoError(t, err) + assert.False(t, r.Refresh) + }) + }) + t.Run("type=return_to", func(t *testing.T) { _, err := login.NewFlow(conf, 0, "csrf", &http.Request{URL: &url.URL{Path: "/", RawQuery: "return_to=https://not-allowed/foobar"}, Host: "ory.sh"}, flow.TypeBrowser) require.Error(t, err) @@ -89,7 +107,8 @@ func TestNewFlow(t *testing.T) { t.Run("case=regular flow creation", func(t *testing.T) { r, err := login.NewFlow(conf, 0, "csrf", &http.Request{ URL: urlx.ParseOrPanic("https://ory.sh/"), - Host: "ory.sh"}, flow.TypeBrowser) + Host: "ory.sh", + }, flow.TypeBrowser) require.NoError(t, err) assert.Equal(t, "https://ory.sh/", r.RequestURL) }) @@ -99,7 +118,8 @@ func TestNewFlow(t *testing.T) { t.Run("case=flow with refresh", func(t *testing.T) { r, err := login.NewFlow(conf, 0, "csrf", &http.Request{ URL: urlx.ParseOrPanic("/?refresh=true"), - Host: "ory.sh"}, flow.TypeAPI) + Host: "ory.sh", + }, flow.TypeAPI) require.NoError(t, err) assert.Equal(t, r.IssuedAt, r.ExpiresAt) assert.Equal(t, flow.TypeAPI, r.Type) @@ -110,7 +130,8 @@ func TestNewFlow(t *testing.T) { t.Run("case=flow without refresh", func(t *testing.T) { r, err := login.NewFlow(conf, 0, "csrf", &http.Request{ URL: urlx.ParseOrPanic("/"), - Host: "ory.sh"}, flow.TypeAPI) + Host: "ory.sh", + }, flow.TypeAPI) require.NoError(t, err) assert.Equal(t, r.IssuedAt, r.ExpiresAt) assert.Equal(t, flow.TypeAPI, r.Type) @@ -129,7 +150,6 @@ func TestNewFlow(t *testing.T) { require.NoError(t, err) assert.Equal(t, "8aadcb8fc1334186a84c4da9813356d9", string(r.OAuth2LoginChallenge)) }) - } func TestFlow(t *testing.T) { diff --git a/selfservice/flow/login/handler.go b/selfservice/flow/login/handler.go index 38eaa26422a7..9c4f7934fd86 100644 --- a/selfservice/flow/login/handler.go +++ b/selfservice/flow/login/handler.go @@ -6,8 +6,11 @@ package login import ( "net/http" "net/url" + "strconv" "time" + "github.com/ory/x/otelx" + "github.com/gofrs/uuid" "github.com/julienschmidt/httprouter" "github.com/pkg/errors" @@ -53,6 +56,7 @@ type ( x.WriterProvider x.CSRFTokenGeneratorProvider x.CSRFProvider + x.TracingProvider config.Provider ErrorHandlerProvider sessiontokenexchange.PersistenceProvider @@ -114,6 +118,12 @@ func WithFormErrorMessage(messages []text.Message) FlowOption { } } +func WithIsAccountLinking() FlowOption { + return func(f *Flow) { + f.isAccountLinkingFlow = true + } +} + func (h *Handler) NewLoginFlow(w http.ResponseWriter, r *http.Request, ft flow.Type, opts ...FlowOption) (*Flow, *session.Session, error) { conf := h.d.Config() f, err := NewFlow(conf, conf.SelfServiceFlowLoginRequestLifespan(r.Context()), h.d.GenerateCSRFToken(r), r, ft) @@ -141,7 +151,9 @@ func (h *Handler) NewLoginFlow(w http.ResponseWriter, r *http.Request, ft flow.T sess, err := h.d.SessionManager().FetchFromRequest(r.Context(), r) if e := new(session.ErrNoActiveSessionFound); errors.As(err, &e) { // No session exists yet - if ft == flow.TypeAPI && r.URL.Query().Get("return_session_token_exchange_code") == "true" { + returnSessionTokenExchangeCode, _ := strconv.ParseBool(r.URL.Query().Get("return_session_token_exchange_code")) + + if ft == flow.TypeAPI && returnSessionTokenExchangeCode { e, err := h.d.SessionTokenExchangePersister().CreateSessionTokenExchanger(r.Context(), f.ID) if err != nil { return nil, nil, errors.WithStack(herodot.ErrInternalServerError.WithWrap(err)) @@ -209,8 +221,39 @@ preLoginHook: } for _, s := range h.d.LoginStrategies(r.Context(), strategyFilters...) { - if err := s.PopulateLoginMethod(r, f.RequestedAAL, f); err != nil { - return nil, nil, err + var populateErr error + + switch strategy := s.(type) { + case FormHydrator: + switch { + case f.RequestedAAL == identity.AuthenticatorAssuranceLevel1: + switch { + case f.IsRefresh(): + // Refreshing takes precedence over identifier_first auth which can not be a refresh flow. + // Therefor this comes first. + populateErr = strategy.PopulateLoginMethodFirstFactorRefresh(r, f) + case h.d.Config().SelfServiceLoginFlowIdentifierFirstEnabled(r.Context()) && !f.isAccountLinkingFlow: + populateErr = strategy.PopulateLoginMethodIdentifierFirstIdentification(r, f) + default: + populateErr = strategy.PopulateLoginMethodFirstFactor(r, f) + } + case f.RequestedAAL == identity.AuthenticatorAssuranceLevel2: + switch { + case f.IsRefresh(): + // Refresh takes precedence. + populateErr = strategy.PopulateLoginMethodSecondFactorRefresh(r, f) + default: + populateErr = strategy.PopulateLoginMethodSecondFactor(r, f) + } + } + case UnifiedFormHydrator: + populateErr = strategy.PopulateLoginMethod(r, f.RequestedAAL, f) + default: + populateErr = errors.WithStack(x.PseudoPanic.WithReasonf("A login strategy was expected to implement one of the interfaces UnifiedFormHydrator or FormHydrator but did not.")) + } + + if populateErr != nil { + return nil, nil, populateErr } } @@ -296,6 +339,9 @@ type createNativeLoginFlow struct { // Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows. // + // DEPRECATED: This field is deprecated. Please remove it from your requests. The user will now see a choice + // of MFA credentials to choose from to perform the second factor instead. + // // in: query Via string `json:"via"` } @@ -335,6 +381,11 @@ type createNativeLoginFlow struct { // 400: errorGeneric // default: errorGeneric func (h *Handler) createNativeLoginFlow(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + var err error + ctx, span := h.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.flow.login.createNativeLoginFlow") + r = r.WithContext(ctx) + defer otelx.End(span, &err) + f, _, err := h.NewLoginFlow(w, r, flow.TypeAPI) if err != nil { h.d.Writer().WriteError(w, r, err) @@ -400,6 +451,14 @@ type createBrowserLoginFlow struct { // required: false // in: query Organization string `json:"organization"` + + // Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows. + // + // DEPRECATED: This field is deprecated. Please remove it from your requests. The user will now see a choice + // of MFA credentials to choose from to perform the second factor instead. + // + // in: query + Via string `json:"via"` } // swagger:route GET /self-service/login/browser frontend createBrowserLoginFlow @@ -441,6 +500,11 @@ type createBrowserLoginFlow struct { // 400: errorGeneric // default: errorGeneric func (h *Handler) createBrowserLoginFlow(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + var err error + ctx, span := h.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.flow.login.createBrowserLoginFlow") + r = r.WithContext(ctx) + defer otelx.End(span, &err) + var ( hydraLoginRequest *hydraclientgo.OAuth2LoginRequest hydraLoginChallenge sqlxx.NullString @@ -449,13 +513,13 @@ func (h *Handler) createBrowserLoginFlow(w http.ResponseWriter, r *http.Request, var err error hydraLoginChallenge, err = hydra.GetLoginChallengeID(h.d.Config(), r) if err != nil { - h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + h.d.SelfServiceErrorManager().Forward(ctx, w, r, err) return } - hydraLoginRequest, err = h.d.Hydra().GetLoginRequest(r.Context(), string(hydraLoginChallenge)) + hydraLoginRequest, err = h.d.Hydra().GetLoginRequest(ctx, string(hydraLoginChallenge)) if err != nil { - h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + h.d.SelfServiceErrorManager().Forward(ctx, w, r, err) return } @@ -471,7 +535,7 @@ func (h *Handler) createBrowserLoginFlow(w http.ResponseWriter, r *http.Request, // different flows, such as login to registration and login to recovery. // After completing a complex flow, such as recovery, we want the user // to be redirected back to the original OAuth2 login flow. - if hydraLoginRequest.RequestUrl != "" && h.d.Config().OAuth2ProviderOverrideReturnTo(r.Context()) { + if hydraLoginRequest.RequestUrl != "" && h.d.Config().OAuth2ProviderOverrideReturnTo(ctx) { // replace the return_to query parameter q := r.URL.Query() q.Set("return_to", hydraLoginRequest.RequestUrl) @@ -483,11 +547,11 @@ func (h *Handler) createBrowserLoginFlow(w http.ResponseWriter, r *http.Request, if errors.Is(err, ErrAlreadyLoggedIn) { if hydraLoginRequest != nil { if !hydraLoginRequest.GetSkip() { - h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrInternalServerError.WithReason("ErrAlreadyLoggedIn indicated we can skip login, but Hydra asked us to refresh"))) + h.d.SelfServiceErrorManager().Forward(ctx, w, r, errors.WithStack(herodot.ErrInternalServerError.WithReason("ErrAlreadyLoggedIn indicated we can skip login, but Hydra asked us to refresh"))) return } - rt, err := h.d.Hydra().AcceptLoginRequest(r.Context(), + rt, err := h.d.Hydra().AcceptLoginRequest(ctx, hydra.AcceptLoginRequestParams{ LoginChallenge: string(hydraLoginChallenge), IdentityID: sess.IdentityID.String(), @@ -495,37 +559,37 @@ func (h *Handler) createBrowserLoginFlow(w http.ResponseWriter, r *http.Request, AuthenticationMethods: sess.AMR, }) if err != nil { - h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + h.d.SelfServiceErrorManager().Forward(ctx, w, r, err) return } returnTo, err := url.Parse(rt) if err != nil { - h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to parse URL: %s", rt))) + h.d.SelfServiceErrorManager().Forward(ctx, w, r, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to parse URL: %s", rt))) return } x.AcceptToRedirectOrJSON(w, r, h.d.Writer(), err, returnTo.String()) return } - returnTo, redirErr := x.SecureRedirectTo(r, h.d.Config().SelfServiceBrowserDefaultReturnTo(r.Context()), - x.SecureRedirectAllowSelfServiceURLs(h.d.Config().SelfPublicURL(r.Context())), - x.SecureRedirectAllowURLs(h.d.Config().SelfServiceBrowserAllowedReturnToDomains(r.Context())), + returnTo, redirErr := x.SecureRedirectTo(r, h.d.Config().SelfServiceBrowserDefaultReturnTo(ctx), + x.SecureRedirectAllowSelfServiceURLs(h.d.Config().SelfPublicURL(ctx)), + x.SecureRedirectAllowURLs(h.d.Config().SelfServiceBrowserAllowedReturnToDomains(ctx)), ) if redirErr != nil { - h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, redirErr) + h.d.SelfServiceErrorManager().Forward(ctx, w, r, redirErr) return } x.AcceptToRedirectOrJSON(w, r, h.d.Writer(), err, returnTo.String()) return } else if err != nil { - h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + h.d.SelfServiceErrorManager().Forward(ctx, w, r, err) return } a.HydraLoginRequest = hydraLoginRequest - x.AcceptToRedirectOrJSON(w, r, h.d.Writer(), a, a.AppendTo(h.d.Config().SelfServiceFlowLoginUI(r.Context())).String()) + x.AcceptToRedirectOrJSON(w, r, h.d.Writer(), a, a.AppendTo(h.d.Config().SelfServiceFlowLoginUI(ctx)).String()) } // Get Login Flow Parameters @@ -594,7 +658,12 @@ type getLoginFlow struct { // 410: errorGeneric // default: errorGeneric func (h *Handler) getLoginFlow(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - ar, err := h.d.LoginFlowPersister().GetLoginFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("id"))) + var err error + ctx, span := h.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.flow.login.getLoginFlow") + r = r.WithContext(ctx) + defer otelx.End(span, &err) + + ar, err := h.d.LoginFlowPersister().GetLoginFlow(ctx, x.ParseUUID(r.URL.Query().Get("id"))) if err != nil { h.d.Writer().WriteError(w, r, err) return @@ -610,7 +679,7 @@ func (h *Handler) getLoginFlow(w http.ResponseWriter, r *http.Request, _ httprou if ar.ExpiresAt.Before(time.Now()) { if ar.Type == flow.TypeBrowser { - redirectURL := flow.GetFlowExpiredRedirectURL(r.Context(), h.d.Config(), RouteInitBrowserFlow, ar.ReturnTo) + redirectURL := flow.GetFlowExpiredRedirectURL(ctx, h.d.Config(), RouteInitBrowserFlow, ar.ReturnTo) h.d.Writer().WriteError(w, r, errors.WithStack(x.ErrGone.WithID(text.ErrIDSelfServiceFlowExpired). WithReason("The login flow has expired. Redirect the user to the login flow init endpoint to initialize a new login flow."). @@ -620,16 +689,16 @@ func (h *Handler) getLoginFlow(w http.ResponseWriter, r *http.Request, _ httprou } h.d.Writer().WriteError(w, r, errors.WithStack(x.ErrGone.WithID(text.ErrIDSelfServiceFlowExpired). WithReason("The login flow has expired. Call the login flow init API endpoint to initialize a new login flow."). - WithDetail("api", urlx.AppendPaths(h.d.Config().SelfPublicURL(r.Context()), RouteInitAPIFlow).String()))) + WithDetail("api", urlx.AppendPaths(h.d.Config().SelfPublicURL(ctx), RouteInitAPIFlow).String()))) return } if ar.OAuth2LoginChallenge != "" { - hlr, err := h.d.Hydra().GetLoginRequest(r.Context(), string(ar.OAuth2LoginChallenge)) + hlr, err := h.d.Hydra().GetLoginRequest(ctx, string(ar.OAuth2LoginChallenge)) if err != nil { // We don't redirect back to the third party on errors because Hydra doesn't // give us the 3rd party return_uri when it redirects to the login UI. - h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err) + h.d.SelfServiceErrorManager().Forward(ctx, w, r, err) return } ar.HydraLoginRequest = hlr @@ -731,19 +800,24 @@ type updateLoginFlowBody struct{} // 422: errorBrowserLocationChangeRequired // default: errorGeneric func (h *Handler) updateLoginFlow(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + var err error + ctx, span := h.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.flow.login.updateLoginFlow") + r = r.WithContext(ctx) + defer otelx.End(span, &err) + rid, err := flow.GetFlowID(r) if err != nil { h.d.LoginFlowErrorHandler().WriteFlowError(w, r, nil, node.DefaultGroup, err) return } - f, err := h.d.LoginFlowPersister().GetLoginFlow(r.Context(), rid) + f, err := h.d.LoginFlowPersister().GetLoginFlow(ctx, rid) if err != nil { h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err) return } - sess, err := h.d.SessionManager().FetchFromRequest(r.Context(), r) + sess, err := h.d.SessionManager().FetchFromRequest(ctx, r) if err == nil { if f.Refresh { // If we want to refresh, continue the login @@ -761,7 +835,7 @@ func (h *Handler) updateLoginFlow(w http.ResponseWriter, r *http.Request, _ http return } - http.Redirect(w, r, h.d.Config().SelfServiceBrowserDefaultReturnTo(r.Context()).String(), http.StatusSeeOther) + http.Redirect(w, r, h.d.Config().SelfServiceBrowserDefaultReturnTo(ctx).String(), http.StatusSeeOther) return } else if e := new(session.ErrNoActiveSessionFound); errors.As(err, &e) { // Only failure scenario here is if we try to upgrade the session to a higher AAL without actually @@ -808,7 +882,7 @@ continueLogin: sess = session.NewInactiveSession() } - method := ss.CompletedAuthenticationMethod(r.Context(), sess.AMR) + method := ss.CompletedAuthenticationMethod(ctx) sess.CompletedLoginForMethod(method) i = interim break diff --git a/selfservice/flow/login/handler_test.go b/selfservice/flow/login/handler_test.go index e5a19556b987..24ac6172897c 100644 --- a/selfservice/flow/login/handler_test.go +++ b/selfservice/flow/login/handler_test.go @@ -21,12 +21,11 @@ import ( "github.com/ory/x/sqlxx" + stdtotp "github.com/pquerna/otp/totp" + "github.com/ory/kratos/hydra" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/strategy/totp" - "github.com/ory/kratos/session" - - stdtotp "github.com/pquerna/otp/totp" "github.com/ory/kratos/ui/container" @@ -466,7 +465,7 @@ func TestFlowLifecycle(t *testing.T) { require.NoError(t, reg.IdentityManager().Update(context.Background(), id, identity.ManagerAllowWriteProtectedTraits)) h := func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - sess, err := session.NewActiveSession(r, id, reg.Config(), time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + sess, err := testhelpers.NewActiveSession(r, reg, id, time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) require.NoError(t, err) sess.AuthenticatorAssuranceLevel = identity.AuthenticatorAssuranceLevel1 require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), sess)) @@ -554,10 +553,21 @@ func TestFlowLifecycle(t *testing.T) { assert.Empty(t, gjson.GetBytes(body, "session_token_exchange_code").String()) }) - t.Run("case=returns session exchange code", func(t *testing.T) { - res, body := initFlow(t, urlx.ParseOrPanic("/?return_session_token_exchange_code=true").Query(), true) - assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow) - assert.NotEmpty(t, gjson.GetBytes(body, "session_token_exchange_code").String()) + t.Run("case=returns session exchange code with any truthy value", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{"https://www.ory.sh", "https://example.com"}) + parameters := []string{"true", "True", "1"} + + for _, param := range parameters { + t.Run("return_session_token_exchange_code="+param, func(t *testing.T) { + res, body := initFlow(t, url.Values{ + "return_session_token_exchange_code": {param}, + "return_to": {"https://example.com/redirect"}, + }, true) + assert.Contains(t, res.Request.URL.String(), login.RouteInitAPIFlow) + assert.NotEmpty(t, gjson.GetBytes(body, "session_token_exchange_code").String()) + assert.Equal(t, "https://example.com/redirect", gjson.GetBytes(body, "return_to").String()) + }) + } }) t.Run("case=can not request refresh and aal at the same time on unauthenticated request", func(t *testing.T) { diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go index f79ab88ff389..8402c2e078f6 100644 --- a/selfservice/flow/login/hook.go +++ b/selfservice/flow/login/hook.go @@ -53,6 +53,7 @@ type ( config.Provider hydra.Provider identity.PrivilegedPoolProvider + identity.ManagementProvider session.ManagementProvider session.PersistenceProvider x.CSRFTokenGeneratorProvider @@ -127,7 +128,7 @@ func (e *HookExecutor) PostLoginHook( w http.ResponseWriter, r *http.Request, g node.UiNodeGroup, - a *Flow, + f *Flow, i *identity.Identity, s *session.Session, provider string, @@ -137,11 +138,11 @@ func (e *HookExecutor) PostLoginHook( r = r.WithContext(ctx) defer otelx.End(span, &err) - if err := e.maybeLinkCredentials(r.Context(), s, i, a); err != nil { + if err := e.maybeLinkCredentials(r.Context(), s, i, f); err != nil { return err } - if err := s.Activate(r, i, e.d.Config(), time.Now().UTC()); err != nil { + if err := e.d.SessionManager().ActivateSession(r, s, i, time.Now().UTC()); err != nil { return err } @@ -149,11 +150,11 @@ func (e *HookExecutor) PostLoginHook( // Verify the redirect URL before we do any other processing. returnTo, err := x.SecureRedirectTo(r, c.SelfServiceBrowserDefaultReturnTo(r.Context()), - x.SecureRedirectReturnTo(a.ReturnTo), - x.SecureRedirectUseSourceURL(a.RequestURL), + x.SecureRedirectReturnTo(f.ReturnTo), + x.SecureRedirectUseSourceURL(f.RequestURL), x.SecureRedirectAllowURLs(c.SelfServiceBrowserAllowedReturnToDomains(r.Context())), x.SecureRedirectAllowSelfServiceURLs(c.SelfPublicURL(r.Context())), - x.SecureRedirectOverrideDefaultReturnTo(c.SelfServiceFlowLoginReturnTo(r.Context(), a.Active.String())), + x.SecureRedirectOverrideDefaultReturnTo(c.SelfServiceFlowLoginReturnTo(r.Context(), f.Active.String())), ) if err != nil { return err @@ -164,44 +165,48 @@ func (e *HookExecutor) PostLoginHook( "redirect_reason": "login successful", })...) + if f.Type == flow.TypeBrowser && x.IsJSONRequest(r) { + f.AddContinueWith(flow.NewContinueWithRedirectBrowserTo(returnTo.String())) + } + classified := s s = s.Declassified() e.d.Logger(). WithRequest(r). WithField("identity_id", i.ID). - WithField("flow_method", a.Active). + WithField("flow_method", f.Active). Debug("Running ExecuteLoginPostHook.") - for k, executor := range e.d.PostLoginHooks(r.Context(), a.Active) { - if err := executor.ExecuteLoginPostHook(w, r, g, a, s); err != nil { + for k, executor := range e.d.PostLoginHooks(r.Context(), f.Active) { + if err := executor.ExecuteLoginPostHook(w, r, g, f, s); err != nil { if errors.Is(err, ErrHookAbortFlow) { e.d.Logger(). WithRequest(r). WithField("executor", fmt.Sprintf("%T", executor)). WithField("executor_position", k). - WithField("executors", PostHookExecutorNames(e.d.PostLoginHooks(r.Context(), a.Active))). + WithField("executors", PostHookExecutorNames(e.d.PostLoginHooks(r.Context(), f.Active))). WithField("identity_id", i.ID). - WithField("flow_method", a.Active). + WithField("flow_method", f.Active). Debug("A ExecuteLoginPostHook hook aborted early.") span.SetAttributes(attribute.String("redirect_reason", "aborted by hook"), attribute.String("executor", fmt.Sprintf("%T", executor))) return nil } - return e.handleLoginError(w, r, g, a, i, err) + return e.handleLoginError(w, r, g, f, i, err) } e.d.Logger(). WithRequest(r). WithField("executor", fmt.Sprintf("%T", executor)). WithField("executor_position", k). - WithField("executors", PostHookExecutorNames(e.d.PostLoginHooks(r.Context(), a.Active))). + WithField("executors", PostHookExecutorNames(e.d.PostLoginHooks(r.Context(), f.Active))). WithField("identity_id", i.ID). - WithField("flow_method", a.Active). + WithField("flow_method", f.Active). Debug("ExecuteLoginPostHook completed successfully.") } - if a.Type == flow.TypeAPI { + if f.Type == flow.TypeAPI { span.SetAttributes(attribute.String("flow_type", string(flow.TypeAPI))) // Fandom-start set session cookie for API flow login -> https://fandom.atlassian.net/browse/PLATFORM-6395 // https://fandom.atlassian.net/wiki/spaces/MOB/pages/1963163864/Fandom+Auth+in+mobile-app @@ -218,23 +223,27 @@ func (e *HookExecutor) PostLoginHook( span.AddEvent(events.NewLoginSucceeded(r.Context(), &events.LoginSucceededOpts{ SessionID: s.ID, IdentityID: i.ID, - FlowType: string(a.Type), - RequestedAAL: string(a.RequestedAAL), - IsRefresh: a.Refresh, - Method: a.Active.String(), + FlowType: string(f.Type), + RequestedAAL: string(f.RequestedAAL), + IsRefresh: f.Refresh, + Method: f.Active.String(), SSOProvider: provider, })) - if a.IDToken != "" { + if f.IDToken != "" { // We don't want to redirect with the code, if the flow was submitted with an ID token. // This is the case for Sign in with native Apple SDK or Google SDK. - } else if handled, err := e.d.SessionManager().MaybeRedirectAPICodeFlow(w, r, a, s.ID, g); err != nil { + } else if handled, err := e.d.SessionManager().MaybeRedirectAPICodeFlow(w, r, f, s.ID, g); err != nil { return errors.WithStack(err) } else if handled { return nil } - response := &APIFlowResponse{Session: s, Token: s.Token} - if required, _ := e.requiresAAL2(r, classified, a); required { + response := &APIFlowResponse{ + Session: s, + Token: s.Token, + ContinueWith: f.ContinueWith(), + } + if required, _ := e.requiresAAL2(r, classified, f); required { // If AAL is not satisfied, we omit the identity to preserve the user's privacy in case of a phishing attack. response.Session.Identity = nil } @@ -255,7 +264,7 @@ func (e *HookExecutor) PostLoginHook( trace.SpanFromContext(r.Context()).AddEvent(events.NewLoginSucceeded(r.Context(), &events.LoginSucceededOpts{ SessionID: s.ID, - IdentityID: i.ID, FlowType: string(a.Type), RequestedAAL: string(a.RequestedAAL), IsRefresh: a.Refresh, Method: a.Active.String(), + IdentityID: i.ID, FlowType: string(f.Type), RequestedAAL: string(f.RequestedAAL), IsRefresh: f.Refresh, Method: f.Active.String(), SSOProvider: provider, })) @@ -266,7 +275,7 @@ func (e *HookExecutor) PostLoginHook( s.Token = "" // If we detect that whoami would require a higher AAL, we redirect! - if _, err := e.requiresAAL2(r, s, a); err != nil { + if _, err := e.requiresAAL2(r, classified, f); err != nil { if aalErr := new(session.ErrAALNotSatisfied); errors.As(err, &aalErr) { span.SetAttributes(attribute.String("return_to", aalErr.RedirectTo), attribute.String("redirect_reason", "requires aal2")) e.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(aalErr.RedirectTo)) @@ -277,10 +286,10 @@ func (e *HookExecutor) PostLoginHook( // If Kratos is used as a Hydra login provider, we need to redirect back to Hydra by returning a 422 status // with the post login challenge URL as the body. - if a.OAuth2LoginChallenge != "" { + if f.OAuth2LoginChallenge != "" { postChallengeURL, err := e.d.Hydra().AcceptLoginRequest(r.Context(), hydra.AcceptLoginRequestParams{ - LoginChallenge: string(a.OAuth2LoginChallenge), + LoginChallenge: string(f.OAuth2LoginChallenge), IdentityID: i.ID.String(), SessionID: s.ID.String(), AuthenticationMethods: s.AMR, @@ -293,13 +302,16 @@ func (e *HookExecutor) PostLoginHook( return nil } - response := &APIFlowResponse{Session: s} + response := &APIFlowResponse{ + Session: s, + ContinueWith: f.ContinueWith(), + } e.d.Writer().Write(w, r, response) return nil } // If we detect that whoami would require a higher AAL, we redirect! - if _, err := e.requiresAAL2(r, s, a); err != nil { + if _, err := e.requiresAAL2(r, classified, f); err != nil { if aalErr := new(session.ErrAALNotSatisfied); errors.As(err, &aalErr) { http.Redirect(w, r, aalErr.RedirectTo, http.StatusSeeOther) return nil @@ -308,10 +320,10 @@ func (e *HookExecutor) PostLoginHook( } finalReturnTo := returnTo.String() - if a.OAuth2LoginChallenge != "" { + if f.OAuth2LoginChallenge != "" { rt, err := e.d.Hydra().AcceptLoginRequest(r.Context(), hydra.AcceptLoginRequestParams{ - LoginChallenge: string(a.OAuth2LoginChallenge), + LoginChallenge: string(f.OAuth2LoginChallenge), IdentityID: i.ID.String(), SessionID: s.ID.String(), AuthenticationMethods: s.AMR, @@ -321,6 +333,9 @@ func (e *HookExecutor) PostLoginHook( } finalReturnTo = rt span.SetAttributes(attribute.String("return_to", rt), attribute.String("redirect_reason", "oauth2 login challenge")) + } else if f.ReturnToVerification != "" { + finalReturnTo = f.ReturnToVerification + span.SetAttributes(attribute.String("redirect_reason", "verification requested")) } x.ContentNegotiationRedirection(w, r, s, e.d.Writer(), finalReturnTo) @@ -374,7 +389,7 @@ func (e *HookExecutor) maybeLinkCredentials(ctx context.Context, sess *session.S return err } - method := strategy.CompletedAuthenticationMethod(ctx, sess.AMR) + method := strategy.CompletedAuthenticationMethod(ctx) sess.CompletedLoginForMethod(method) return nil diff --git a/selfservice/flow/login/hook_test.go b/selfservice/flow/login/hook_test.go index 10bd3fa61a03..29b00c1ef5d7 100644 --- a/selfservice/flow/login/hook_test.go +++ b/selfservice/flow/login/hook_test.go @@ -101,6 +101,17 @@ func TestLoginExecutor(t *testing.T) { assert.EqualValues(t, "https://www.ory.sh/", res.Request.URL.String()) }) + t.Run("case=pass without hooks if client is ajax", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + + ts := newServer(t, flow.TypeBrowser, nil) + res, body := makeRequestPost(t, ts, true, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), ts.URL) + assert.EqualValues(t, gjson.Get(body, "continue_with").Raw, `[{"action":"redirect_browser_to","redirect_browser_to":"https://www.ory.sh/"}]`) + t.Logf("%s", body) + }) + t.Run("case=pass if hooks pass", func(t *testing.T) { t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) viperSetPost(t, conf, strategy.String(), []config.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}}) @@ -294,6 +305,7 @@ func TestLoginExecutor(t *testing.T) { }) }) }) + t.Run("case=maybe links credential", func(t *testing.T) { t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) @@ -308,9 +320,7 @@ func TestLoginExecutor(t *testing.T) { require.NoError(t, reg.Persister().CreateIdentity(context.Background(), useIdentity)) credsOIDC, err := identity.NewCredentialsOIDC( - "id-token", - "access-token", - "refresh-token", + &identity.CredentialsOIDCEncryptedTokens{IDToken: "id-token", AccessToken: "access-token", RefreshToken: "refresh-token"}, "my-provider", email, "", diff --git a/selfservice/flow/login/session.go b/selfservice/flow/login/session.go index 8b99d037e15a..35e2aff87b38 100644 --- a/selfservice/flow/login/session.go +++ b/selfservice/flow/login/session.go @@ -3,7 +3,10 @@ package login -import "github.com/ory/kratos/session" +import ( + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/session" +) // The Response for Login Flows via API // @@ -26,4 +29,12 @@ type APIFlowResponse struct { // // required: true Session *session.Session `json:"session"` + + // Contains a list of actions, that could follow this flow + // + // It can, for example, this will contain a reference to the verification flow, created as part of the user's + // registration or the token of the session. + // + // required: false + ContinueWith []flow.ContinueWith `json:"continue_with"` } diff --git a/selfservice/flow/login/sort.go b/selfservice/flow/login/sort.go index 9f1a144ffc2d..c9d9145d46e3 100644 --- a/selfservice/flow/login/sort.go +++ b/selfservice/flow/login/sort.go @@ -15,6 +15,7 @@ func sortNodes(ctx context.Context, n node.Nodes) error { node.OpenIDConnectGroup, node.DefaultGroup, node.WebAuthnGroup, + node.PasskeyGroup, node.CodeGroup, node.PasswordGroup, node.TOTPGroup, diff --git a/selfservice/flow/login/strategy.go b/selfservice/flow/login/strategy.go index c70ad9cc8684..8ea671343c76 100644 --- a/selfservice/flow/login/strategy.go +++ b/selfservice/flow/login/strategy.go @@ -20,9 +20,8 @@ type Strategy interface { ID() identity.CredentialsType NodeGroup() node.UiNodeGroup RegisterLoginRoutes(*x.RouterPublic) - PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *Flow) error Login(w http.ResponseWriter, r *http.Request, f *Flow, sess *session.Session) (i *identity.Identity, err error) - CompletedAuthenticationMethod(ctx context.Context, methods session.AuthenticationMethods) session.AuthenticationMethod + CompletedAuthenticationMethod(ctx context.Context) session.AuthenticationMethod } type Strategies []Strategy diff --git a/selfservice/flow/login/strategy_form_hydrator.go b/selfservice/flow/login/strategy_form_hydrator.go new file mode 100644 index 000000000000..098c195b5df4 --- /dev/null +++ b/selfservice/flow/login/strategy_form_hydrator.go @@ -0,0 +1,67 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "net/http" + + "github.com/pkg/errors" + + "github.com/ory/kratos/identity" +) + +type UnifiedFormHydrator interface { + PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *Flow) error +} + +type FormHydrator interface { + PopulateLoginMethodFirstFactorRefresh(r *http.Request, sr *Flow) error + PopulateLoginMethodFirstFactor(r *http.Request, sr *Flow) error + PopulateLoginMethodSecondFactor(r *http.Request, sr *Flow) error + PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *Flow) error + + // PopulateLoginMethodIdentifierFirstCredentials populates the login form with the first factor credentials. + // This method is called when the login flow is set to identifier first. The method will receive information + // about the identity that is being used to log in and the identifier that was used to find the identity. + // + // The method should populate the login form with the credentials of the identity. + // + // If the method can not find any credentials (because the identity does not exist) idfirst.ErrNoCredentialsFound + // must be returned. When returning idfirst.ErrNoCredentialsFound the strategy will appropriately deal with + // account enumeration mitigation. + // + // This method does however need to take appropriate steps to show/hide certain fields depending on the account + // enumeration configuration. + PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, sr *Flow, options ...FormHydratorModifier) error + PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, sr *Flow) error +} + +var ErrBreakLoginPopulate = errors.New("skip rest of login form population") + +type FormHydratorOptions struct { + IdentityHint *identity.Identity + Identifier string +} + +type FormHydratorModifier func(o *FormHydratorOptions) + +func WithIdentityHint(i *identity.Identity) FormHydratorModifier { + return func(o *FormHydratorOptions) { + o.IdentityHint = i + } +} + +func WithIdentifier(i string) FormHydratorModifier { + return func(o *FormHydratorOptions) { + o.Identifier = i + } +} + +func NewFormHydratorOptions(modifiers []FormHydratorModifier) *FormHydratorOptions { + o := new(FormHydratorOptions) + for _, m := range modifiers { + m(o) + } + return o +} diff --git a/selfservice/flow/login/strategy_form_hydrator_test.go b/selfservice/flow/login/strategy_form_hydrator_test.go new file mode 100644 index 000000000000..863a1031051d --- /dev/null +++ b/selfservice/flow/login/strategy_form_hydrator_test.go @@ -0,0 +1,24 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ory/kratos/identity" +) + +func TestWithIdentityHint(t *testing.T) { + expected := new(identity.Identity) + opts := NewFormHydratorOptions([]FormHydratorModifier{WithIdentityHint(expected)}) + assert.Equal(t, expected, opts.IdentityHint) +} + +func TestWithIdentifier(t *testing.T) { + expected := "identifier" + opts := NewFormHydratorOptions([]FormHydratorModifier{WithIdentifier(expected)}) + assert.Equal(t, expected, opts.Identifier) +} diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json index f4c0270da2dc..17eb6e965bcb 100644 --- a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json +++ b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json @@ -50,8 +50,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json index 56782eed4571..a9ad1e527fb4 100644 --- a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json +++ b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json @@ -50,8 +50,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json index f4c0270da2dc..17eb6e965bcb 100644 --- a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json +++ b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json @@ -50,8 +50,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json index 56782eed4571..a9ad1e527fb4 100644 --- a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json +++ b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json @@ -50,8 +50,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/flow/recovery/error.go b/selfservice/flow/recovery/error.go index 9f93af941447..837bc6e0e4b3 100644 --- a/selfservice/flow/recovery/error.go +++ b/selfservice/flow/recovery/error.go @@ -64,10 +64,12 @@ func (s *ErrorHandler) WriteFlowError( group node.UiNodeGroup, recoveryErr error, ) { - s.d.Audit(). + logger := s.d.Audit(). WithError(recoveryErr). WithRequest(r). - WithField("recovery_flow", f). + WithField("recovery_flow", f.ToLoggerField()) + + logger. Info("Encountered self-service recovery error.") if f == nil { diff --git a/selfservice/flow/recovery/error_test.go b/selfservice/flow/recovery/error_test.go index 432d149a04f1..6a7414050d6e 100644 --- a/selfservice/flow/recovery/error_test.go +++ b/selfservice/flow/recovery/error_test.go @@ -88,7 +88,7 @@ func TestHandleError(t *testing.T) { defer res.Body.Close() require.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()+"?id=") - sse, _, err := sdk.FrontendApi.GetFlowError(context.Background()).Id(res.Request.URL.Query().Get("id")).Execute() + sse, _, err := sdk.FrontendAPI.GetFlowError(context.Background()).Id(res.Request.URL.Query().Get("id")).Execute() require.NoError(t, err) return sse.Error, nil @@ -346,7 +346,7 @@ func TestHandleError_WithContinueWith(t *testing.T) { defer res.Body.Close() require.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()+"?id=") - sse, _, err := sdk.FrontendApi.GetFlowError(context.Background()).Id(res.Request.URL.Query().Get("id")).Execute() + sse, _, err := sdk.FrontendAPI.GetFlowError(context.Background()).Id(res.Request.URL.Query().Get("id")).Execute() require.NoError(t, err) return sse.Error, nil diff --git a/selfservice/flow/recovery/flow.go b/selfservice/flow/recovery/flow.go index d5aed79ae0f3..7dc1845a77b6 100644 --- a/selfservice/flow/recovery/flow.go +++ b/selfservice/flow/recovery/flow.go @@ -103,6 +103,11 @@ type Flow struct { // Contains possible actions that could follow this flow ContinueWith []flow.ContinueWith `json:"continue_with,omitempty" faker:"-" db:"-"` + + // TransientPayload is used to pass data from the recovery flow to hooks and email templates + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" faker:"-" db:"-"` } var _ flow.Flow = new(Flow) @@ -239,3 +244,22 @@ func (f *Flow) GetFlowName() flow.FlowName { func (f *Flow) SetState(state State) { f.State = state } + +func (t *Flow) GetTransientPayload() json.RawMessage { + return t.TransientPayload +} + +func (f *Flow) ToLoggerField() map[string]interface{} { + if f == nil { + return map[string]interface{}{} + } + return map[string]interface{}{ + "id": f.ID.String(), + "return_to": f.ReturnTo, + "request_url": f.RequestURL, + "active": f.Active, + "type": f.Type, + "nid": f.NID, + "state": f.State, + } +} diff --git a/selfservice/flow/recovery/handler.go b/selfservice/flow/recovery/handler.go index 6927a733dd8e..d5ba1a44dc47 100644 --- a/selfservice/flow/recovery/handler.go +++ b/selfservice/flow/recovery/handler.go @@ -458,6 +458,7 @@ func (h *Handler) updateRecoveryFlow(w http.ResponseWriter, r *http.Request, ps h.d.RecoveryFlowErrorHandler().WriteFlowError(w, r, f, g, err) return } + updatedFlow.TransientPayload = f.TransientPayload h.d.Writer().Write(w, r, updatedFlow) } diff --git a/selfservice/flow/recovery/hook.go b/selfservice/flow/recovery/hook.go index 15c3078e918c..212eb061b7f4 100644 --- a/selfservice/flow/recovery/hook.go +++ b/selfservice/flow/recovery/hook.go @@ -81,10 +81,14 @@ func NewHookExecutor(d executorDependencies) *HookExecutor { } func (e *HookExecutor) PostRecoveryHook(w http.ResponseWriter, r *http.Request, a *Flow, s *session.Session) error { - e.d.Logger(). - WithRequest(r). - WithField("identity_id", s.Identity.ID). - Debug("Running ExecutePostRecoveryHooks.") + logger := e.d.Logger(). + WithRequest(r) + + if s.Identity != nil { + logger = logger.WithField("identity_id", s.Identity.ID) + } + + logger.Debug("Running ExecutePostRecoveryHooks.") for k, executor := range e.d.PostRecoveryHooks(r.Context()) { if err := executor.ExecutePostRecoveryHook(w, r, a, s); err != nil { var traits identity.Traits @@ -94,20 +98,16 @@ func (e *HookExecutor) PostRecoveryHook(w http.ResponseWriter, r *http.Request, return flow.HandleHookError(w, r, a, traits, node.LinkGroup, err, e.d, e.d) } - e.d.Logger().WithRequest(r). + logger. WithField("executor", fmt.Sprintf("%T", executor)). WithField("executor_position", k). WithField("executors", PostHookRecoveryExecutorNames(e.d.PostRecoveryHooks(r.Context()))). - WithField("identity_id", s.Identity.ID). Debug("ExecutePostRecoveryHook completed successfully.") } trace.SpanFromContext(r.Context()).AddEvent(events.NewRecoverySucceeded(r.Context(), s.Identity.ID, string(a.Type), a.Active.String())) - e.d.Logger(). - WithRequest(r). - WithField("identity_id", s.Identity.ID). - Debug("Post recovery execution hooks completed successfully.") + logger.Debug("Post recovery execution hooks completed successfully.") return nil } diff --git a/selfservice/flow/recovery/hook_test.go b/selfservice/flow/recovery/hook_test.go index deb4b0426363..ce4ccf6deb76 100644 --- a/selfservice/flow/recovery/hook_test.go +++ b/selfservice/flow/recovery/hook_test.go @@ -10,8 +10,6 @@ import ( "testing" "time" - "github.com/ory/kratos/session" - "github.com/ory/kratos/selfservice/flow/recovery" "github.com/ory/kratos/selfservice/strategy/code" @@ -31,6 +29,7 @@ import ( func TestRecoveryExecutor(t *testing.T) { ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") s := code.NewStrategy(reg) newServer := func(t *testing.T, i *identity.Identity, ft flow.Type) *httptest.Server { @@ -46,13 +45,14 @@ func TestRecoveryExecutor(t *testing.T) { router.GET("/recovery/post", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { a, err := recovery.NewFlow(conf, time.Minute, x.FakeCSRFToken, r, s, ft) require.NoError(t, err) - s, _ := session.NewActiveSession(r, + s, err := testhelpers.NewActiveSession(r, + reg, i, - conf, time.Now().UTC(), identity.CredentialsTypeRecoveryLink, identity.AuthenticatorAssuranceLevel1, ) + require.NoError(t, err) a.RequestURL = x.RequestURL(r).String() if testhelpers.SelfServiceHookErrorHandler(t, w, r, recovery.ErrHookAbortFlow, reg.RecoveryExecutor().PostRecoveryHook(w, r, a, s)) { _, _ = w.Write([]byte("ok")) diff --git a/selfservice/flow/recovery/test/persistence.go b/selfservice/flow/recovery/test/persistence.go index 8bc9efad88e0..75dd6b00c6b2 100644 --- a/selfservice/flow/recovery/test/persistence.go +++ b/selfservice/flow/recovery/test/persistence.go @@ -12,7 +12,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ory/kratos/driver/config" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" "github.com/ory/kratos/selfservice/flow" @@ -23,7 +22,7 @@ import ( "github.com/ory/x/sqlcon" ) -func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { +func TestFlowPersister(ctx context.Context, p interface { persistence.Persister }, ) func(t *testing.T) { @@ -33,7 +32,6 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") t.Run("case=should error when the recovery request does not exist", func(t *testing.T) { _, err := p.GetRecoveryFlow(ctx, x.NewUUID()) diff --git a/selfservice/flow/registration/error.go b/selfservice/flow/registration/error.go index b3ef3d9054df..41a15f08b2b1 100644 --- a/selfservice/flow/registration/error.go +++ b/selfservice/flow/registration/error.go @@ -72,6 +72,7 @@ func (s *ErrorHandler) PrepareReplacementForExpiredFlow(w http.ResponseWriter, r return e.WithFlow(a), nil } + func (s *ErrorHandler) WriteFlowError( w http.ResponseWriter, r *http.Request, @@ -79,15 +80,16 @@ func (s *ErrorHandler) WriteFlowError( group node.UiNodeGroup, err error, ) { - if dup := new(identity.ErrDuplicateCredentials); errors.As(err, &dup) { err = schema.NewDuplicateCredentialsError(dup) } - s.d.Audit(). + logger := s.d.Audit(). WithError(err). WithRequest(r). - WithField("registration_flow", f). + WithField("registration_flow", f.ToLoggerField()) + + logger. Info("Encountered self-service flow error.") if f == nil { diff --git a/selfservice/flow/registration/error_test.go b/selfservice/flow/registration/error_test.go index 4b5464f88652..02fdd9fb1e4c 100644 --- a/selfservice/flow/registration/error_test.go +++ b/selfservice/flow/registration/error_test.go @@ -87,7 +87,7 @@ func TestHandleError(t *testing.T) { defer res.Body.Close() require.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()+"?id=") - sse, _, err := sdk.FrontendApi.GetFlowError(context.Background()).Id(res.Request.URL.Query().Get("id")).Execute() + sse, _, err := sdk.FrontendAPI.GetFlowError(context.Background()).Id(res.Request.URL.Query().Get("id")).Execute() require.NoError(t, err) return sse.Error, nil diff --git a/selfservice/flow/registration/flow.go b/selfservice/flow/registration/flow.go index b3dae8a6a1c5..5b39dd76f750 100644 --- a/selfservice/flow/registration/flow.go +++ b/selfservice/flow/registration/flow.go @@ -98,6 +98,8 @@ type Flow struct { OrganizationID uuid.NullUUID `json:"organization_id,omitempty" faker:"-" db:"organization_id"` // TransientPayload is used to pass data from the registration to a webhook + // + // required: false TransientPayload json.RawMessage `json:"transient_payload,omitempty" faker:"-" db:"-"` // Contains a list of actions, that could follow this flow @@ -269,3 +271,26 @@ func (f *Flow) GetFlowName() flow.FlowName { func (f *Flow) SetState(state State) { f.State = state } + +func (f *Flow) GetTransientPayload() json.RawMessage { + return f.TransientPayload +} + +func (f *Flow) SetReturnToVerification(to string) { + f.ReturnToVerification = to +} + +func (f *Flow) ToLoggerField() map[string]interface{} { + if f == nil { + return map[string]interface{}{} + } + return map[string]interface{}{ + "id": f.ID.String(), + "return_to": f.ReturnTo, + "request_url": f.RequestURL, + "active": f.Active, + "Type": f.Type, + "nid": f.NID, + "state": f.State, + } +} diff --git a/selfservice/flow/registration/handler.go b/selfservice/flow/registration/handler.go index 8cfe59e4d6d9..ee3e23ba144f 100644 --- a/selfservice/flow/registration/handler.go +++ b/selfservice/flow/registration/handler.go @@ -663,7 +663,7 @@ func (h *Handler) updateRegistrationFlow(w http.ResponseWriter, r *http.Request, return } - if err := h.d.RegistrationExecutor().PostRegistrationHook(w, r, s.ID(), "", f, i); err != nil { + if err := h.d.RegistrationExecutor().PostRegistrationHook(w, r, s.ID(), "", "", f, i); err != nil { h.d.RegistrationFlowErrorHandler().WriteFlowError(w, r, f, s.NodeGroup(), err) return } diff --git a/selfservice/flow/registration/hook.go b/selfservice/flow/registration/hook.go index cdc1e7b45f1f..e199e1440b85 100644 --- a/selfservice/flow/registration/hook.go +++ b/selfservice/flow/registration/hook.go @@ -9,7 +9,6 @@ import ( "net/http" "time" - "github.com/julienschmidt/httprouter" "github.com/pkg/errors" "go.opentelemetry.io/otel/attribute" @@ -101,7 +100,7 @@ func NewHookExecutor(d executorDependencies) *HookExecutor { return &HookExecutor{d: d} } -func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Request, ct identity.CredentialsType, provider string, registrationFlow *Flow, i *identity.Identity) (err error) { +func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Request, ct identity.CredentialsType, provider, organizationID string, registrationFlow *Flow, i *identity.Identity) (err error) { ctx := r.Context() ctx, span := e.d.Tracer(ctx).Tracer().Start(ctx, "HookExecutor.PostRegistrationHook") r = r.WithContext(ctx) @@ -192,12 +191,17 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque if err != nil { return err } + span.SetAttributes(otelx.StringAttrs(map[string]string{ "return_to": returnTo.String(), - "flow_type": string(flow.TypeBrowser), + "flow_type": string(registrationFlow.Type), "redirect_reason": "registration successful", })...) + if registrationFlow.Type == flow.TypeBrowser && x.IsJSONRequest(r) { + registrationFlow.AddContinueWith(flow.NewContinueWithRedirectBrowserTo(returnTo.String())) + } + e.d.Audit(). WithRequest(r). WithField("identity_id", i.ID). @@ -207,9 +211,8 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque s := session.NewInactiveSession() - s.CompletedLoginForWithProvider(ct, identity.AuthenticatorAssuranceLevel1, provider, - httprouter.ParamsFromContext(r.Context()).ByName("organization")) - if err := s.Activate(r, i, c, time.Now().UTC()); err != nil { + s.CompletedLoginForWithProvider(ct, identity.AuthenticatorAssuranceLevel1, provider, organizationID) + if err := e.d.SessionManager().ActivateSession(r, s, i, time.Now().UTC()); err != nil { return err } diff --git a/selfservice/flow/registration/hook_test.go b/selfservice/flow/registration/hook_test.go index 3761692e3f45..9a65b05a0eeb 100644 --- a/selfservice/flow/registration/hook_test.go +++ b/selfservice/flow/registration/hook_test.go @@ -65,7 +65,7 @@ func TestRegistrationExecutor(t *testing.T) { for _, callback := range flowCallbacks { callback(regFlow) } - _ = handleErr(t, w, r, reg.RegistrationHookExecutor().PostRegistrationHook(w, r, identity.CredentialsType(strategy), "", regFlow, i)) + _ = handleErr(t, w, r, reg.RegistrationHookExecutor().PostRegistrationHook(w, r, identity.CredentialsType(strategy), "", "", regFlow, i)) }) ts := httptest.NewServer(router) @@ -91,6 +91,21 @@ func TestRegistrationExecutor(t *testing.T) { assert.Equal(t, actual.Traits, i.Traits) }) + t.Run("case=pass without hooks if ajax client", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + i := testhelpers.SelfServiceHookFakeIdentity(t) + + ts := newServer(t, i, flow.TypeBrowser) + res, body := makeRequestPost(t, ts, true, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), ts.URL) + assert.EqualValues(t, gjson.Get(body, "continue_with").Raw, `[{"action":"redirect_browser_to","redirect_browser_to":"https://www.ory.sh/"}]`) + + actual, err := reg.IdentityPool().GetIdentity(context.Background(), i.ID, identity.ExpandNothing) + require.NoError(t, err) + assert.Equal(t, actual.Traits, i.Traits) + }) + t.Run("case=pass if hooks pass", func(t *testing.T) { t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) viperSetPost(t, conf, strategy, []config.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}}) diff --git a/selfservice/flow/registration/sort.go b/selfservice/flow/registration/sort.go index 15348dd56512..f68dbb79ca59 100644 --- a/selfservice/flow/registration/sort.go +++ b/selfservice/flow/registration/sort.go @@ -13,11 +13,13 @@ func SortNodes(ctx context.Context, n node.Nodes, schemaRef string) error { return n.SortBySchema(ctx, node.SortBySchema(schemaRef), node.SortByGroups([]node.UiNodeGroup{ - node.DefaultGroup, node.OpenIDConnectGroup, + node.DefaultGroup, node.WebAuthnGroup, + node.PasskeyGroup, node.CodeGroup, node.PasswordGroup, + node.ProfileGroup, }), node.SortUpdateOrder(node.PasswordLoginOrder), node.SortUseOrderAppend([]string{ diff --git a/selfservice/flow/request.go b/selfservice/flow/request.go index bb140eae24a2..a4c5cb74740d 100644 --- a/selfservice/flow/request.go +++ b/selfservice/flow/request.go @@ -9,6 +9,9 @@ import ( "net/http" "strings" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "github.com/ory/kratos/driver/config" "github.com/ory/kratos/selfservice/strategy" "github.com/ory/x/decoderx" @@ -100,8 +103,9 @@ func MethodEnabledAndAllowedFromRequest(r *http.Request, flow FlowName, expected return MethodEnabledAndAllowed(r.Context(), flow, expected, method.Method, d) } -func MethodEnabledAndAllowed(ctx context.Context, flowName FlowName, expected, actual string, d config.Provider) error { +func MethodEnabledAndAllowed(ctx context.Context, _ FlowName, expected, actual string, d config.Provider) error { if actual != expected { + trace.SpanFromContext(ctx).SetAttributes(attribute.String("not_responsible_reason", "method mismatch")) return errors.WithStack(ErrStrategyNotResponsible) } diff --git a/selfservice/flow/settings/error.go b/selfservice/flow/settings/error.go index 2fd190c888e9..aaf6076de0be 100644 --- a/selfservice/flow/settings/error.go +++ b/selfservice/flow/settings/error.go @@ -4,7 +4,6 @@ package settings import ( - "context" "net/http" "net/url" @@ -43,7 +42,7 @@ type ( HandlerProvider FlowPersistenceProvider - IdentityTraitsSchemas(ctx context.Context) (schema.Schemas, error) + schema.IdentitySchemaProvider } ErrorHandlerProvider interface{ SettingsFlowErrorHandler() *ErrorHandler } @@ -141,11 +140,12 @@ func (s *ErrorHandler) WriteFlowError( id *identity.Identity, err error, ) { - s.d.Audit(). + logger := s.d.Audit(). WithError(err). WithRequest(r). - WithField("settings_flow", f). - Info("Encountered self-service settings error.") + WithField("settings_flow", f.ToLoggerField()) + + logger.Info("Encountered self-service settings error.") shouldRespondWithJSON := x.IsJSONRequest(r) if f != nil && f.Type == flow.TypeAPI { diff --git a/selfservice/flow/settings/error_test.go b/selfservice/flow/settings/error_test.go index 5776cd2b6942..41b0c1e50104 100644 --- a/selfservice/flow/settings/error_test.go +++ b/selfservice/flow/settings/error_test.go @@ -11,6 +11,8 @@ import ( "testing" "time" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/pkg/errors" "github.com/gofrs/uuid" @@ -101,7 +103,7 @@ func TestHandleError(t *testing.T) { defer res.Body.Close() require.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()+"?id=") - sse, _, err := sdk.FrontendApi.GetFlowError(context.Background()). + sse, _, err := sdk.FrontendAPI.GetFlowError(context.Background()). Id(res.Request.URL.Query().Get("id")).Execute() require.NoError(t, err) @@ -149,11 +151,12 @@ func TestHandleError(t *testing.T) { t.Cleanup(reset) req := httptest.NewRequest("GET", "/sessions/whoami", nil) + req.WithContext(confighelpers.WithConfigValue(ctx, config.ViperKeySessionLifespan, time.Hour)) // This needs an authenticated client in order to call the RouteGetFlow endpoint - s, err := session.NewActiveSession(req, &id, testhelpers.NewSessionLifespanProvider(time.Hour), time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + s, err := testhelpers.NewActiveSession(req, reg, &id, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) require.NoError(t, err) - c := testhelpers.NewHTTPClientWithSessionToken(t, reg, s) + c := testhelpers.NewHTTPClientWithSessionToken(t, ctx, reg, s) settingsFlow = newFlow(t, time.Minute, tc.t) flowError = flow.NewFlowExpiredError(expiredAnHourAgo) diff --git a/selfservice/flow/settings/flow.go b/selfservice/flow/settings/flow.go index a96da053d766..b32b4676effb 100644 --- a/selfservice/flow/settings/flow.go +++ b/selfservice/flow/settings/flow.go @@ -119,6 +119,11 @@ type Flow struct { // // required: false ContinueWithItems []flow.ContinueWith `json:"continue_with,omitempty" db:"-" faker:"-" ` + + // TransientPayload is used to pass data from the settings flow to hooks and email templates + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" faker:"-" db:"-"` } var _ flow.Flow = new(Flow) @@ -194,8 +199,8 @@ func (f *Flow) Valid(s *session.Session) error { } if f.IdentityID != s.Identity.ID { - return errors.WithStack(herodot.ErrBadRequest.WithID(text.ErrIDInitiatedBySomeoneElse).WithReasonf( - "You must restart the flow because the resumable session was initiated by another person.")) + return errors.WithStack(herodot.ErrForbidden.WithID(text.ErrIDInitiatedBySomeoneElse).WithReasonf( + "The request was initiated by someone else and has been blocked for security reasons. Please go back and try again.")) } return nil @@ -256,3 +261,22 @@ func (f *Flow) GetFlowName() flow.FlowName { func (f *Flow) SetState(state State) { f.State = state } + +func (t *Flow) GetTransientPayload() json.RawMessage { + return t.TransientPayload +} + +func (f *Flow) ToLoggerField() map[string]interface{} { + if f == nil { + return map[string]interface{}{} + } + return map[string]interface{}{ + "id": f.ID.String(), + "return_to": f.ReturnTo, + "request_url": f.RequestURL, + "active": f.Active, + "Type": f.Type, + "nid": f.NID, + "state": f.State, + } +} diff --git a/selfservice/flow/settings/handler.go b/selfservice/flow/settings/handler.go index 8b96ce85369a..efc1e5a84bc3 100644 --- a/selfservice/flow/settings/handler.go +++ b/selfservice/flow/settings/handler.go @@ -68,7 +68,7 @@ type ( HookExecutorProvider x.CSRFTokenGeneratorProvider - schema.IdentityTraitsProvider + schema.IdentitySchemaProvider login.HandlerProvider } diff --git a/selfservice/flow/settings/handler_test.go b/selfservice/flow/settings/handler_test.go index a24e598c64aa..9d4b3e670a17 100644 --- a/selfservice/flow/settings/handler_test.go +++ b/selfservice/flow/settings/handler_test.go @@ -67,8 +67,8 @@ func TestHandler(t *testing.T) { primaryIdentity := &identity.Identity{ID: x.NewUUID(), Traits: identity.Traits(`{}`)} require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), primaryIdentity)) - primaryUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, primaryIdentity) - otherUser := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg) + primaryUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, primaryIdentity) + otherUser := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg) newExpiredFlow := func() *settings.Flow { f, err := settings.NewFlow(conf, -time.Minute, @@ -133,7 +133,7 @@ func TestHandler(t *testing.T) { return initAuthenticatedFlow(t, hc, false, true, opts...) } - aal2Identity := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, &identity.Identity{ + aal2Identity := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, &identity.Identity{ State: identity.StateActive, Traits: []byte(`{"email":"foo@bar"}`), Credentials: map[identity.CredentialsType]identity.Credentials{ @@ -151,7 +151,7 @@ func TestHandler(t *testing.T) { }) t.Run("description=success", func(t *testing.T) { - user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg) + user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg) res, body := initFlow(t, user1, true) assert.Contains(t, res.Request.URL.String(), settings.RouteInitAPIFlow) assertion(t, body, true) @@ -206,7 +206,7 @@ func TestHandler(t *testing.T) { }) t.Run("description=success", func(t *testing.T) { - user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg) + user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg) res, body := initFlow(t, user1, false) assert.Contains(t, res.Request.URL.String(), reg.Config().SelfServiceFlowSettingsUI(ctx).String()) assertion(t, body, false) @@ -241,7 +241,7 @@ func TestHandler(t *testing.T) { }) t.Run("case=redirects with 303", func(t *testing.T) { - c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg) + c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg) // prevent the redirect c.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse @@ -268,7 +268,7 @@ func TestHandler(t *testing.T) { }) t.Run("description=success", func(t *testing.T) { - user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg) + user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg) res, body := initSPAFlow(t, user1) assert.Contains(t, res.Request.URL.String(), settings.RouteInitBrowserFlow) assertion(t, body, false) @@ -277,7 +277,7 @@ func TestHandler(t *testing.T) { t.Run("description=can not init if identity has aal2 but session has aal1", func(t *testing.T) { email := testhelpers.RandomEmail() conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL) - user1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, &identity.Identity{ + user1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, &identity.Identity{ State: identity.StateActive, Traits: []byte(`{"email":"` + email + `"}`), Credentials: map[identity.CredentialsType]identity.Credentials{ @@ -303,7 +303,7 @@ func TestHandler(t *testing.T) { t.Run("description=settings return_to should persist through mfa flows", func(t *testing.T) { email := testhelpers.RandomEmail() conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL) - user1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, &identity.Identity{ + user1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, &identity.Identity{ State: identity.StateActive, Traits: []byte(`{"email":"` + email + `"}`), Credentials: map[identity.CredentialsType]identity.Credentials{ @@ -334,7 +334,7 @@ func TestHandler(t *testing.T) { t.Run("endpoint=fetch", func(t *testing.T) { t.Run("description=fetching a non-existent flow should return a 404 error", func(t *testing.T) { - _, _, err := testhelpers.NewSDKCustomClient(publicTS, otherUser).FrontendApi.GetSettingsFlow(context.Background()).Id("i-do-not-exist").Execute() + _, _, err := testhelpers.NewSDKCustomClient(publicTS, otherUser).FrontendAPI.GetSettingsFlow(context.Background()).Id("i-do-not-exist").Execute() require.Error(t, err) require.IsTypef(t, new(kratos.GenericOpenAPIError), err, "%T", err) @@ -345,7 +345,7 @@ func TestHandler(t *testing.T) { pr := newExpiredFlow() require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(context.Background(), pr)) - _, _, err := testhelpers.NewSDKCustomClient(publicTS, primaryUser).FrontendApi.GetSettingsFlow(context.Background()).Id(pr.ID.String()).Execute() + _, _, err := testhelpers.NewSDKCustomClient(publicTS, primaryUser).FrontendAPI.GetSettingsFlow(context.Background()).Id(pr.ID.String()).Execute() require.Error(t, err) require.IsTypef(t, new(kratos.GenericOpenAPIError), err, "%T", err) @@ -356,7 +356,7 @@ func TestHandler(t *testing.T) { returnTo := "https://www.ory.sh" conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnTo}) - client := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg) + client := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg) body := testhelpers.EasyGetBody(t, client, publicTS.URL+settings.RouteInitBrowserFlow+"?return_to="+returnTo) // Expire the flow @@ -385,8 +385,8 @@ func TestHandler(t *testing.T) { t.Run("description=should fail to fetch request if identity changed", func(t *testing.T) { t.Run("type=api", func(t *testing.T) { - user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg) - user2 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg) + user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg) + user2 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg) res, err := user1.Get(publicTS.URL + settings.RouteInitAPIFlow) require.NoError(t, err) @@ -413,7 +413,7 @@ func TestHandler(t *testing.T) { rid := res.Request.URL.Query().Get("flow") require.NotEmpty(t, rid) - _, _, err = testhelpers.NewSDKCustomClient(publicTS, otherUser).FrontendApi.GetSettingsFlow(context.Background()).Id(rid).Execute() + _, _, err = testhelpers.NewSDKCustomClient(publicTS, otherUser).FrontendAPI.GetSettingsFlow(context.Background()).Id(rid).Execute() require.Error(t, err) require.IsTypef(t, new(kratos.GenericOpenAPIError), err, "%T", err) assert.Equal(t, int64(http.StatusForbidden), gjson.GetBytes(err.(*kratos.GenericOpenAPIError).Body(), "error.code").Int(), "should return a 403 error because the identities from the cookies do not match") @@ -537,39 +537,39 @@ func TestHandler(t *testing.T) { t.Run("description=fail to submit form as another user", func(t *testing.T) { t.Run("type=api", func(t *testing.T) { - user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg) - user2 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg) + user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg) + user2 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg) _, body := initFlow(t, user1, true) var f kratos.SettingsFlow require.NoError(t, json.Unmarshal(body, &f)) actual, res := testhelpers.SettingsMakeRequest(t, true, false, &f, user2, `{"method":"not-exists"}`) - assert.Equal(t, http.StatusBadRequest, res.StatusCode) - assert.Equal(t, "You must restart the flow because the resumable session was initiated by another person.", gjson.Get(actual, "ui.messages.0.text").String(), actual) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Equal(t, "The request was initiated by someone else and has been blocked for security reasons. Please go back and try again.", gjson.Get(actual, "error.reason").String(), actual) }) t.Run("type=spa", func(t *testing.T) { - user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg) - user2 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg) + user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg) + user2 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg) _, body := initFlow(t, user1, true) var f kratos.SettingsFlow require.NoError(t, json.Unmarshal(body, &f)) actual, res := testhelpers.SettingsMakeRequest(t, false, true, &f, user2, `{"method":"not-exists"}`) - assert.Equal(t, http.StatusBadRequest, res.StatusCode) - assert.Equal(t, "You must restart the flow because the resumable session was initiated by another person.", gjson.Get(actual, "ui.messages.0.text").String(), actual) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Equal(t, "The request was initiated by someone else and has been blocked for security reasons. Please go back and try again.", gjson.Get(actual, "error.reason").String(), actual) }) t.Run("type=browser", func(t *testing.T) { - user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg) - user2 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg) + user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg) + user2 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg) _, body := initFlow(t, user1, true) var f kratos.SettingsFlow require.NoError(t, json.Unmarshal(body, &f)) actual, res := testhelpers.SettingsMakeRequest(t, false, false, &f, user2, `{"method":"not-exists"}`) - assert.Equal(t, http.StatusBadRequest, res.StatusCode) - assert.Equal(t, "You must restart the flow because the resumable session was initiated by another person.", gjson.Get(actual, "ui.messages.0.text").String(), actual) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Equal(t, "The request was initiated by someone else and has been blocked for security reasons. Please go back and try again.", gjson.Get(actual, "error.reason").String(), actual) }) }) @@ -580,7 +580,7 @@ func TestHandler(t *testing.T) { require.NoError(t, json.Unmarshal(body, &f)) actual, res := testhelpers.SettingsMakeRequest(t, false, true, &f, primaryUser, fmt.Sprintf(`{"method":"profile", "numby": 15, "csrf_token": "%s"}`, x.FakeCSRFToken)) - assert.Equal(t, http.StatusOK, res.StatusCode) + require.Equal(t, http.StatusOK, res.StatusCode) require.Len(t, primaryUser.Jar.Cookies(urlx.ParseOrPanic(publicTS.URL+login.RouteGetFlow)), 1) require.Contains(t, fmt.Sprintf("%v", primaryUser.Jar.Cookies(urlx.ParseOrPanic(publicTS.URL))), "ory_kratos_session") assert.Equal(t, "Your changes have been saved!", gjson.Get(actual, "ui.messages.0.text").String(), actual) @@ -602,7 +602,7 @@ func TestHandler(t *testing.T) { t.Run("case=relative redirect when self-service settings ui is a relative url", func(t *testing.T) { reg.Config().MustSet(ctx, config.ViperKeySelfServiceSettingsURL, "/settings-ts") - user1 := testhelpers.NewNoRedirectHTTPClientWithArbitrarySessionCookie(t, reg) + user1 := testhelpers.NewNoRedirectHTTPClientWithArbitrarySessionCookie(t, ctx, reg) res, _ := initFlow(t, user1, false) assert.Regexp( t, diff --git a/selfservice/flow/settings/hook.go b/selfservice/flow/settings/hook.go index 61b8a766fc6a..487d508d411c 100644 --- a/selfservice/flow/settings/hook.go +++ b/selfservice/flow/settings/hook.go @@ -282,7 +282,8 @@ func (e *HookExecutor) PostSettingsHook(w http.ResponseWriter, r *http.Request, WithField("flow_method", settingsType). Debug("Completed all PostSettingsPrePersistHooks and PostSettingsPostPersistHooks.") - trace.SpanFromContext(r.Context()).AddEvent(events.NewSettingsSucceeded(r.Context(), i.ID, string(ctxUpdate.Flow.Type), ctxUpdate.Flow.Active.String())) + trace.SpanFromContext(r.Context()).AddEvent(events.NewSettingsSucceeded( + r.Context(), i.ID, string(ctxUpdate.Flow.Type), settingsType)) if ctxUpdate.Flow.Type == flow.TypeAPI { updatedFlow, err := e.d.SettingsFlowPersister().GetSettingsFlow(r.Context(), ctxUpdate.Flow.ID) @@ -308,6 +309,7 @@ func (e *HookExecutor) PostSettingsHook(w http.ResponseWriter, r *http.Request, } // ContinueWith items are transient items, not stored in the database, and need to be carried over here, so // they can be returned to the client. + ctxUpdate.Flow.AddContinueWith(flow.NewContinueWithRedirectBrowserTo(returnTo.String())) updatedFlow.ContinueWithItems = ctxUpdate.Flow.ContinueWithItems e.d.Writer().Write(w, r, updatedFlow) diff --git a/selfservice/flow/settings/hook_test.go b/selfservice/flow/settings/hook_test.go index 0ab28a1504f9..242eacf0e8da 100644 --- a/selfservice/flow/settings/hook_test.go +++ b/selfservice/flow/settings/hook_test.go @@ -24,7 +24,6 @@ import ( "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/selfservice/hook" - "github.com/ory/kratos/session" "github.com/ory/kratos/x" ) @@ -54,7 +53,7 @@ func TestSettingsExecutor(t *testing.T) { if i == nil { i = testhelpers.SelfServiceHookCreateFakeIdentity(t, reg) } - sess, _ := session.NewActiveSession(r, i, conf, time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + sess, _ := testhelpers.NewActiveSession(r, reg, i, time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) f, err := settings.NewFlow(conf, time.Minute, r, sess.Identity, ft) require.NoError(t, err) @@ -67,7 +66,7 @@ func TestSettingsExecutor(t *testing.T) { if i == nil { i = testhelpers.SelfServiceHookCreateFakeIdentity(t, reg) } - sess, _ := session.NewActiveSession(r, i, conf, time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + sess, _ := testhelpers.NewActiveSession(r, reg, i, time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) a, err := settings.NewFlow(conf, time.Minute, r, sess.Identity, ft) require.NoError(t, err) @@ -101,6 +100,16 @@ func TestSettingsExecutor(t *testing.T) { assert.Contains(t, res.Request.URL.String(), uiURL) }) + t.Run("case=pass without hooks if ajax client", func(t *testing.T) { + t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) + + ts := newServer(t, nil, flow.TypeBrowser) + res, body := makeRequestPost(t, ts, true, url.Values{}) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.Contains(t, res.Request.URL.String(), ts.URL) + assert.EqualValues(t, gjson.Get(body, "continue_with.0.action").String(), "redirect_browser_to") + }) + t.Run("case=pass if hooks pass", func(t *testing.T) { t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf)) diff --git a/selfservice/flow/settings/test/persistence.go b/selfservice/flow/settings/test/persistence.go index 85c80e49d74e..498419a65c3a 100644 --- a/selfservice/flow/settings/test/persistence.go +++ b/selfservice/flow/settings/test/persistence.go @@ -27,7 +27,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ory/kratos/driver/config" "github.com/ory/kratos/x" ) @@ -37,12 +36,10 @@ func clearids(r *settings.Flow) { r.IdentityID = uuid.Nil } -func TestFlowPersister(ctx context.Context, conf *config.Config, p persistence.Persister) func(t *testing.T) { +func TestFlowPersister(ctx context.Context, p persistence.Persister) func(t *testing.T) { return func(t *testing.T) { _, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") - t.Run("case=should error when the settings request does not exist", func(t *testing.T) { _, err := p.GetSettingsFlow(ctx, x.NewUUID()) require.Error(t, err) diff --git a/selfservice/flow/state.go b/selfservice/flow/state.go index 76a0683fc19d..a6b4f1a98966 100644 --- a/selfservice/flow/state.go +++ b/selfservice/flow/state.go @@ -33,7 +33,11 @@ const ( StateSuccess State = "success" ) -var states = []State{StateChooseMethod, StateEmailSent, StatePassedChallenge} +var states = []State{ + StateChooseMethod, + StateEmailSent, + StatePassedChallenge, +} func indexOf(current State) int { for k, s := range states { diff --git a/selfservice/flow/type.go b/selfservice/flow/type.go index 2f0726b0352e..4206344ed7a9 100644 --- a/selfservice/flow/type.go +++ b/selfservice/flow/type.go @@ -22,3 +22,11 @@ func (t Type) IsBrowser() bool { func (t Type) IsAPI() bool { return t == TypeAPI } + +func (t Type) Valid() bool { + switch t { + case TypeAPI, TypeBrowser: + return true + } + return false +} diff --git a/selfservice/flow/verification/error.go b/selfservice/flow/verification/error.go index b39875b233d0..0fcffe84869e 100644 --- a/selfservice/flow/verification/error.go +++ b/selfservice/flow/verification/error.go @@ -60,10 +60,12 @@ func (s *ErrorHandler) WriteFlowError( group node.UiNodeGroup, err error, ) { - s.d.Audit(). + logger := s.d.Audit(). WithError(err). WithRequest(r). - WithField("verification_flow", f). + WithField("verification_flow", f.ToLoggerField()) + + logger. Info("Encountered self-service verification error.") if f == nil { diff --git a/selfservice/flow/verification/error_test.go b/selfservice/flow/verification/error_test.go index 86f4d2e1e32a..7359f8c81e1d 100644 --- a/selfservice/flow/verification/error_test.go +++ b/selfservice/flow/verification/error_test.go @@ -87,7 +87,7 @@ func TestHandleError(t *testing.T) { defer res.Body.Close() require.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()+"?id=") - sse, _, err := sdk.FrontendApi.GetFlowError(context.Background()).Id(res.Request.URL.Query().Get("id")).Execute() + sse, _, err := sdk.FrontendAPI.GetFlowError(context.Background()).Id(res.Request.URL.Query().Get("id")).Execute() require.NoError(t, err) return sse.Error, nil diff --git a/selfservice/flow/verification/flow.go b/selfservice/flow/verification/flow.go index 264e356da476..c82ac9148430 100644 --- a/selfservice/flow/verification/flow.go +++ b/selfservice/flow/verification/flow.go @@ -92,6 +92,11 @@ type Flow struct { // UpdatedAt is a helper struct field for gobuffalo.pop. UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` NID uuid.UUID `json:"-" faker:"-" db:"nid"` + + // TransientPayload is used to pass data from the verification flow to hooks and email templates + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" faker:"-" db:"-"` } type OAuth2LoginChallengeParams struct { @@ -180,6 +185,7 @@ func NewPostHookFlow(conf *config.Config, exp time.Duration, csrf string, r *htt if err != nil { return nil, err } + f.TransientPayload = original.GetTransientPayload() requestURL, err := url.ParseRequestURI(original.GetRequestURL()) if err != nil { requestURL = new(url.URL) @@ -288,3 +294,22 @@ func (f *Flow) GetFlowName() flow.FlowName { func (f *Flow) SetState(state State) { f.State = state } + +func (t *Flow) GetTransientPayload() json.RawMessage { + return t.TransientPayload +} + +func (f *Flow) ToLoggerField() map[string]interface{} { + if f == nil { + return map[string]interface{}{} + } + return map[string]interface{}{ + "id": f.ID.String(), + "return_to": f.ReturnTo, + "request_url": f.RequestURL, + "active": f.Active, + "Type": f.Type, + "nid": f.NID, + "state": f.State, + } +} diff --git a/selfservice/flow/verification/test/persistence.go b/selfservice/flow/verification/test/persistence.go index 57c35cba8d2e..a021d06a152e 100644 --- a/selfservice/flow/verification/test/persistence.go +++ b/selfservice/flow/verification/test/persistence.go @@ -12,7 +12,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ory/kratos/driver/config" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" "github.com/ory/kratos/selfservice/flow" @@ -23,7 +22,7 @@ import ( "github.com/ory/x/sqlcon" ) -func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { +func TestFlowPersister(ctx context.Context, p interface { persistence.Persister }, ) func(t *testing.T) { @@ -34,8 +33,6 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p interface { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") - t.Run("case=should error when the verification request does not exist", func(t *testing.T) { _, err := p.GetVerificationFlow(ctx, x.NewUUID()) require.Error(t, err) diff --git a/selfservice/flowhelpers/login.go b/selfservice/flowhelpers/login.go index 2e97f85ebe30..60c17176a740 100644 --- a/selfservice/flowhelpers/login.go +++ b/selfservice/flowhelpers/login.go @@ -15,11 +15,11 @@ func GuessForcedLoginIdentifier(r *http.Request, d interface { session.ManagementProvider identity.PrivilegedPoolProvider }, f interface { - IsForced() bool + IsRefresh() bool }, ct identity.CredentialsType) (identifier string, id *identity.Identity, creds *identity.Credentials) { var ok bool // This block adds the identifier to the method when the request is forced - as a hint for the user. - if !f.IsForced() { + if !f.IsRefresh() { // do nothing } else if sess, err := d.SessionManager().FetchFromRequest(r.Context(), r); err != nil { // do nothing diff --git a/selfservice/flowhelpers/login_test.go b/selfservice/flowhelpers/login_test.go index 3506c73c8513..0a400ff7d4b7 100644 --- a/selfservice/flowhelpers/login_test.go +++ b/selfservice/flowhelpers/login_test.go @@ -17,7 +17,6 @@ import ( "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flowhelpers" - "github.com/ory/kratos/session" ) func TestGuessForcedLoginIdentifier(t *testing.T) { @@ -34,9 +33,9 @@ func TestGuessForcedLoginIdentifier(t *testing.T) { req := httptest.NewRequest("GET", "/sessions/whoami", nil) - sess, err := session.NewActiveSession(req, i, conf, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + sess, err := testhelpers.NewActiveSession(req, reg, i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) require.NoError(t, err) - reg.SessionPersister().UpsertSession(context.Background(), sess) + require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), sess)) r := httptest.NewRequest("GET", "/login", nil) r.Header.Set("Authorization", "Bearer "+sess.Token) diff --git a/selfservice/hook/code_address_verifier_test.go b/selfservice/hook/code_address_verifier_test.go index 01b09bea4c50..cfdf2ae841d3 100644 --- a/selfservice/hook/code_address_verifier_test.go +++ b/selfservice/hook/code_address_verifier_test.go @@ -40,7 +40,7 @@ func TestCodeAddressVerifier(t *testing.T) { _, err := reg.RegistrationCodePersister().CreateRegistrationCode(ctx, &code.CreateRegistrationCodeParams{ Address: address, - AddressType: identity.CodeAddressTypeEmail, + AddressType: identity.AddressTypeEmail, RawCode: rawCode, ExpiresIn: time.Hour, FlowID: rf.ID, diff --git a/selfservice/hook/hooks.go b/selfservice/hook/hooks.go index 7bed664b94d7..70973fc3a111 100644 --- a/selfservice/hook/hooks.go +++ b/selfservice/hook/hooks.go @@ -4,13 +4,14 @@ package hook const ( - KeySessionIssuer = "session" - KeySessionDestroyer = "revoke_active_sessions" - KeyWebHook = "web_hook" - KeyAddressVerifier = "require_verified_address" - KeyVerificationUI = "show_verification_ui" + KeySessionIssuer = "session" + KeySessionDestroyer = "revoke_active_sessions" + KeyWebHook = "web_hook" + KeyAddressVerifier = "require_verified_address" + KeyVerificationUI = "show_verification_ui" + KeyTwoStepRegistration = "two_step_registration" + KeyVerifier = "verification" // fandom-start - KeyTotpLookupSecretsDestroyer = "totp_destroys_lookup_secrets" // nolint:gosec // fandom-end ) diff --git a/selfservice/hook/hooktest/web_hook_test_server.go b/selfservice/hook/hooktest/web_hook_test_server.go new file mode 100644 index 000000000000..6111b48540a7 --- /dev/null +++ b/selfservice/hook/hooktest/web_hook_test_server.go @@ -0,0 +1,74 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hooktest + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/http/httptest" + "slices" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/x/configx" + "github.com/ory/x/ioutilx" +) + +var jsonnet = base64.StdEncoding.EncodeToString([]byte("function(ctx) ctx")) + +type Server struct { + *httptest.Server + LastBody []byte +} + +// NewServer returns a new webhook server for testing. +func NewServer() *Server { + s := new(Server) + httptestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s.LastBody = ioutilx.MustReadAll(r.Body) + w.WriteHeader(http.StatusOK) + })) + + s.Server = httptestServer + + return s +} + +// HookConfig returns the hook configuration for calling the webhook server. +func (s *Server) HookConfig() config.SelfServiceHook { + return config.SelfServiceHook{ + Name: "web_hook", + Config: []byte(fmt.Sprintf(` +{ + "method": "POST", + "url": "%s", + "body": "base64://%s" +}`, s.URL, jsonnet)), + } +} + +func (s *Server) AssertTransientPayload(t *testing.T, expected string) { + require.NotEmpty(t, s.LastBody) + actual := gjson.GetBytes(s.LastBody, "flow.transient_payload").String() + assert.JSONEq(t, expected, actual, "%+v", actual) +} + +// SetConfig adds the webhook to the list of hooks for the given key and restores +// the original configuration after the test. +func (s *Server) SetConfig(t *testing.T, conf *configx.Provider, key string) { + var newValue []config.SelfServiceHook + original := conf.Get(key) + if originalHooks, ok := original.([]config.SelfServiceHook); ok { + newValue = slices.Clone(originalHooks) + } + require.NoError(t, conf.Set(key, append(newValue, s.HookConfig()))) + t.Cleanup(func() { + _ = conf.Set(key, original) + }) +} diff --git a/selfservice/hook/password_migration_hook.go b/selfservice/hook/password_migration_hook.go new file mode 100644 index 000000000000..065dc5dcddc6 --- /dev/null +++ b/selfservice/hook/password_migration_hook.go @@ -0,0 +1,111 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.11.0" + "go.opentelemetry.io/otel/trace" + grpccodes "google.golang.org/grpc/codes" + + "github.com/ory/herodot" + "github.com/ory/kratos/request" + "github.com/ory/kratos/schema" + "github.com/ory/x/otelx" +) + +type ( + PasswordMigration struct { + deps webHookDependencies + conf json.RawMessage + } + PasswordMigrationRequest struct { + Identifier string `json:"identifier"` + Password string `json:"password"` + } + PasswordMigrationResponse struct { + Status string `json:"status"` + } +) + +func NewPasswordMigrationHook(deps webHookDependencies, conf json.RawMessage) *PasswordMigration { + return &PasswordMigration{deps: deps, conf: conf} +} + +func (p *PasswordMigration) Execute(ctx context.Context, data *PasswordMigrationRequest) (err error) { + var ( + httpClient = p.deps.HTTPClient(ctx) + emitEvent = gjson.GetBytes(p.conf, "emit_analytics_event").Bool() || !gjson.GetBytes(p.conf, "emit_analytics_event").Exists() // default true + tracer = trace.SpanFromContext(ctx).TracerProvider().Tracer("kratos-webhooks") + ) + + ctx, span := tracer.Start(ctx, "selfservice.login.password_migration") + defer otelx.End(span, &err) + + if emitEvent { + instrumentHTTPClientForEvents(ctx, httpClient) + } + builder, err := request.NewBuilder(ctx, p.conf, p.deps, nil) + if err != nil { + return errors.WithStack(err) + } + req, err := builder.BuildRequest(ctx, nil) // passing a nil body here skips Jsonnet + if err != nil { + return errors.WithStack(err) + } + rawData, err := json.Marshal(data) + if err != nil { + return errors.WithStack(err) + } + if err = req.SetBody(rawData); err != nil { + return errors.WithStack(err) + } + + p.deps.Logger().WithRequest(req.Request).Info("Dispatching password migration hook") + req = req.WithContext(ctx) + + resp, err := httpClient.Do(req) + if err != nil { + return herodot.DefaultError{ + CodeField: http.StatusBadGateway, + StatusField: http.StatusText(http.StatusBadGateway), + GRPCCodeField: grpccodes.Aborted, + ReasonField: "A third-party upstream service could not be reached. Please try again later.", + ErrorField: "calling the password migration hook failed", + }.WithWrap(errors.WithStack(err)) + } + defer resp.Body.Close() + span.SetAttributes(semconv.HTTPAttributesFromHTTPStatusCode(resp.StatusCode)...) + + switch resp.StatusCode { + case http.StatusOK: + // We now check if the response matches `{"status": "password_match" }`. + dec := json.NewDecoder(io.LimitReader(resp.Body, 1024)) // limit the response body to 1KB + var response PasswordMigrationResponse + if err := dec.Decode(&response); err != nil || response.Status != "password_match" { + return errors.WithStack(schema.NewInvalidCredentialsError()) + } + return nil + + case http.StatusForbidden: + return errors.WithStack(schema.NewInvalidCredentialsError()) + default: + span.SetStatus(codes.Error, "Unexpected HTTP status code") + return herodot.DefaultError{ + CodeField: http.StatusBadGateway, + StatusField: http.StatusText(http.StatusBadGateway), + GRPCCodeField: grpccodes.Aborted, + ReasonField: "A third-party upstream service responded improperly. Please try again later.", + ErrorField: fmt.Sprintf("password migration hook failed with status code %v", resp.StatusCode), + } + } +} diff --git a/selfservice/hook/session_issuer.go b/selfservice/hook/session_issuer.go index b9dfbfaa9df9..03a87489a979 100644 --- a/selfservice/hook/session_issuer.go +++ b/selfservice/hook/session_issuer.go @@ -7,8 +7,11 @@ import ( "context" "net/http" + "go.opentelemetry.io/otel/trace" + "github.com/ory/kratos/identity" "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x/events" "github.com/pkg/errors" @@ -21,9 +24,7 @@ import ( "github.com/ory/x/otelx" ) -var ( - _ registration.PostHookPostPersistExecutor = new(SessionIssuer) -) +var _ registration.PostHookPostPersistExecutor = new(SessionIssuer) type ( sessionIssuerDependencies interface { @@ -70,6 +71,14 @@ func (e *SessionIssuer) executePostRegistrationPostPersistHook(w http.ResponseWr Identity: s.Identity, ContinueWith: a.ContinueWithItems, }) + + trace.SpanFromContext(r.Context()).AddEvent(events.NewLoginSucceeded(r.Context(), &events.LoginSucceededOpts{ + SessionID: s.ID, + IdentityID: s.Identity.ID, + FlowType: string(a.Type), + Method: a.Active.String(), + })) + return errors.WithStack(registration.ErrHookAbortFlow) } @@ -78,6 +87,13 @@ func (e *SessionIssuer) executePostRegistrationPostPersistHook(w http.ResponseWr return err } + trace.SpanFromContext(r.Context()).AddEvent(events.NewLoginSucceeded(r.Context(), &events.LoginSucceededOpts{ + SessionID: s.ID, + IdentityID: s.Identity.ID, + FlowType: string(a.Type), + Method: a.Active.String(), + })) + // SPA flows additionally send the session if x.IsJSONRequest(r) { e.r.Writer().Write(w, r, ®istration.APIFlowResponse{ diff --git a/selfservice/hook/show_verification_ui.go b/selfservice/hook/show_verification_ui.go index 8aebe212a9f3..dcca0d0669c9 100644 --- a/selfservice/hook/show_verification_ui.go +++ b/selfservice/hook/show_verification_ui.go @@ -11,13 +11,18 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" "github.com/ory/x/otelx" ) -var _ registration.PostHookPostPersistExecutor = new(ShowVerificationUIHook) +var ( + _ registration.PostHookPostPersistExecutor = new(ShowVerificationUIHook) + _ login.PostHookExecutor = new(ShowVerificationUIHook) +) type ( showVerificationUIDependencies interface { @@ -47,7 +52,20 @@ func (e *ShowVerificationUIHook) ExecutePostRegistrationPostPersistHook(_ http.R }) } -func (e *ShowVerificationUIHook) execute(r *http.Request, f *registration.Flow) error { +// ExecuteLoginPostHook adds redirect headers and status code if the request is a browser request. +// If the request is not a browser request, this hook does nothing. +func (e *ShowVerificationUIHook) ExecuteLoginPostHook(_ http.ResponseWriter, r *http.Request, _ node.UiNodeGroup, f *login.Flow, _ *session.Session) error { + return otelx.WithSpan(r.Context(), "selfservice.hook.ShowVerificationUIHook.ExecutePostRegistrationPostPersistHook", func(ctx context.Context) error { + return e.execute(r.WithContext(ctx), f) + }) +} + +type loginOrRegistrationFlow interface { + ContinueWith() []flow.ContinueWith + SetReturnToVerification(string) +} + +func (e *ShowVerificationUIHook) execute(r *http.Request, f loginOrRegistrationFlow) error { if !x.IsBrowserRequest(r) { // this hook is only intended to be used by browsers, as it redirects to the verification ui // JSON API clients should use the `continue_with` field to continue the flow @@ -55,7 +73,7 @@ func (e *ShowVerificationUIHook) execute(r *http.Request, f *registration.Flow) } var vf *flow.ContinueWithVerificationUI - for _, c := range f.ContinueWithItems { + for _, c := range f.ContinueWith() { if item, ok := c.(*flow.ContinueWithVerificationUI); ok { vf = item } @@ -64,7 +82,7 @@ func (e *ShowVerificationUIHook) execute(r *http.Request, f *registration.Flow) ctx := r.Context() if vf != nil { redirURL := e.d.Config().SelfServiceFlowVerificationUI(ctx) - f.ReturnToVerification = vf.AppendTo(redirURL).String() + f.SetReturnToVerification(vf.AppendTo(redirURL).String()) } return nil diff --git a/selfservice/hook/show_verification_ui_test.go b/selfservice/hook/show_verification_ui_test.go index 7f1c54a8d775..446295d703eb 100644 --- a/selfservice/hook/show_verification_ui_test.go +++ b/selfservice/hook/show_verification_ui_test.go @@ -17,62 +17,120 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/internal" "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/selfservice/hook" ) func TestExecutePostRegistrationPostPersistHook(t *testing.T) { - t.Run("case=no continue with items returns 200 OK", func(t *testing.T) { - _, reg := internal.NewVeryFastRegistryWithoutDB(t) - h := hook.NewShowVerificationUIHook(reg) - browserRequest := httptest.NewRequest("GET", "/", nil) - f := ®istration.Flow{} - rec := httptest.NewRecorder() - require.NoError(t, h.ExecutePostRegistrationPostPersistHook(rec, browserRequest, f, nil, identity.CredentialsTypePassword)) - require.Equal(t, 200, rec.Code) - }) + t.Run("flow=registration", func(t *testing.T) { + t.Run("case=no continue with items returns 200 OK", func(t *testing.T) { + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + f := ®istration.Flow{} + rec := httptest.NewRecorder() + require.NoError(t, h.ExecutePostRegistrationPostPersistHook(rec, browserRequest, f, nil)) + require.Equal(t, 200, rec.Code) + }) - t.Run("case=not a browser request returns 200 OK", func(t *testing.T) { - _, reg := internal.NewVeryFastRegistryWithoutDB(t) - h := hook.NewShowVerificationUIHook(reg) - browserRequest := httptest.NewRequest("GET", "/", nil) - browserRequest.Header.Add("Accept", "application/json") - f := ®istration.Flow{} - rec := httptest.NewRecorder() - require.NoError(t, h.ExecutePostRegistrationPostPersistHook(rec, browserRequest, f, nil, identity.CredentialsTypePassword)) - require.Equal(t, 200, rec.Code) - }) + t.Run("case=not a browser request returns 200 OK", func(t *testing.T) { + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + browserRequest.Header.Add("Accept", "application/json") + f := ®istration.Flow{} + rec := httptest.NewRecorder() + require.NoError(t, h.ExecutePostRegistrationPostPersistHook(rec, browserRequest, f, nil)) + require.Equal(t, 200, rec.Code) + }) + + t.Run("case=verification ui in continue with item returns redirect", func(t *testing.T) { + conf, reg := internal.NewVeryFastRegistryWithoutDB(t) + conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUI, "/verification") + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + vf := &verification.Flow{ + ID: uuid.Must(uuid.NewV4()), + } + rf := ®istration.Flow{} + rf.ContinueWithItems = []flow.ContinueWith{ + flow.NewContinueWithVerificationUI(vf, "some@ory.sh", ""), + } + rec := httptest.NewRecorder() + require.NoError(t, h.ExecutePostRegistrationPostPersistHook(rec, browserRequest, rf, nil)) + assert.Equal(t, 200, rec.Code) + assert.Equal(t, "/verification?flow="+vf.ID.String(), rf.ReturnToVerification) + }) - t.Run("case=verification ui in continue with item returns redirect", func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUI, "/verification") - h := hook.NewShowVerificationUIHook(reg) - browserRequest := httptest.NewRequest("GET", "/", nil) - vf := &verification.Flow{ - ID: uuid.Must(uuid.NewV4()), - } - rf := ®istration.Flow{} - rf.ContinueWithItems = []flow.ContinueWith{ - flow.NewContinueWithVerificationUI(vf, "some@ory.sh", ""), - } - rec := httptest.NewRecorder() - require.NoError(t, h.ExecutePostRegistrationPostPersistHook(rec, browserRequest, rf, nil, identity.CredentialsTypePassword)) - assert.Equal(t, 200, rec.Code) - assert.Equal(t, "/verification?flow="+vf.ID.String(), rf.ReturnToVerification) + t.Run("case=no verification ui in continue with item returns 200 OK", func(t *testing.T) { + conf, reg := internal.NewVeryFastRegistryWithoutDB(t) + conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUI, "/verification") + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + rf := ®istration.Flow{} + rf.ContinueWithItems = []flow.ContinueWith{ + flow.NewContinueWithSetToken("token"), + } + rec := httptest.NewRecorder() + require.NoError(t, h.ExecutePostRegistrationPostPersistHook(rec, browserRequest, rf, nil)) + assert.Equal(t, 200, rec.Code) + }) }) - t.Run("case=no verification ui in continue with item returns 200 OK", func(t *testing.T) { - conf, reg := internal.NewVeryFastRegistryWithoutDB(t) - conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUI, "/verification") - h := hook.NewShowVerificationUIHook(reg) - browserRequest := httptest.NewRequest("GET", "/", nil) - rf := ®istration.Flow{} - rf.ContinueWithItems = []flow.ContinueWith{ - flow.NewContinueWithSetToken("token"), - } - rec := httptest.NewRecorder() - require.NoError(t, h.ExecutePostRegistrationPostPersistHook(rec, browserRequest, rf, nil, identity.CredentialsTypePassword)) - assert.Equal(t, 200, rec.Code) + t.Run("flow=login", func(t *testing.T) { + t.Run("case=no continue with items returns 200 OK", func(t *testing.T) { + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + f := &login.Flow{} + rec := httptest.NewRecorder() + require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", f, nil)) + require.Equal(t, 200, rec.Code) + }) + + t.Run("case=not a browser request returns 200 OK", func(t *testing.T) { + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + browserRequest.Header.Add("Accept", "application/json") + f := &login.Flow{} + rec := httptest.NewRecorder() + require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", f, nil)) + require.Equal(t, 200, rec.Code) + }) + + t.Run("case=verification ui in continue with item returns redirect", func(t *testing.T) { + conf, reg := internal.NewVeryFastRegistryWithoutDB(t) + conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUI, "/verification") + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + vf := &verification.Flow{ + ID: uuid.Must(uuid.NewV4()), + } + rf := &login.Flow{} + rf.ContinueWithItems = []flow.ContinueWith{ + flow.NewContinueWithVerificationUI(vf, "some@ory.sh", ""), + } + rec := httptest.NewRecorder() + require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", rf, nil)) + assert.Equal(t, 200, rec.Code) + assert.Equal(t, "/verification?flow="+vf.ID.String(), rf.ReturnToVerification) + }) + + t.Run("case=no verification ui in continue with item returns 200 OK", func(t *testing.T) { + conf, reg := internal.NewVeryFastRegistryWithoutDB(t) + conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUI, "/verification") + h := hook.NewShowVerificationUIHook(reg) + browserRequest := httptest.NewRequest("GET", "/", nil) + rf := &login.Flow{} + rf.ContinueWithItems = []flow.ContinueWith{ + flow.NewContinueWithSetToken("token"), + } + rec := httptest.NewRecorder() + require.NoError(t, h.ExecuteLoginPostHook(rec, browserRequest, "", rf, nil)) + assert.Equal(t, 200, rec.Code) + }) }) } diff --git a/selfservice/hook/stub/test_body.jsonnet b/selfservice/hook/stub/test_body.jsonnet index 409fa13695ca..117ecc587707 100644 --- a/selfservice/hook/stub/test_body.jsonnet +++ b/selfservice/hook/stub/test_body.jsonnet @@ -1,6 +1,7 @@ function(ctx) std.prune({ flow_id: ctx.flow.id, identity_id: if std.objectHas(ctx, "identity") then ctx.identity.id, + session_id: if std.objectHas(ctx, "session") then ctx.session.id, headers: ctx.request_headers, url: ctx.request_url, method: ctx.request_method, diff --git a/selfservice/hook/two_step_registration.go b/selfservice/hook/two_step_registration.go new file mode 100644 index 000000000000..def83d5cc7e7 --- /dev/null +++ b/selfservice/hook/two_step_registration.go @@ -0,0 +1,58 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package hook + +import ( + "net/http" + + "github.com/pkg/errors" + "github.com/tidwall/sjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" +) + +var _ registration.PreHookExecutor = new(TwoStepRegistration) + +type ( + twoStepRegistrationDeps interface { + x.WriterProvider + config.Provider + } + + TwoStepRegistration struct { + d twoStepRegistrationDeps + } +) + +func NewTwoStepRegistration(d twoStepRegistrationDeps) *TwoStepRegistration { + return &TwoStepRegistration{d: d} +} + +func (e *TwoStepRegistration) ExecuteRegistrationPreHook(_ http.ResponseWriter, _ *http.Request, regFlow *registration.Flow) (err error) { + stepOneNodes := make([]*node.Node, 0, len(regFlow.UI.Nodes)) + stepTwoNodes := make([]*node.Node, 0, len(regFlow.UI.Nodes)) + for _, n := range regFlow.UI.Nodes { + if n.Group == node.ProfileGroup || n.Group == node.OpenIDConnectGroup || n.Group == node.DefaultGroup { + stepOneNodes = append(stepOneNodes, n) + } else { + stepTwoNodes = append(stepTwoNodes, n) + } + } + + regFlow.UI.Nodes = stepOneNodes + + regFlow.InternalContext, err = sjson.SetBytes(regFlow.InternalContext, "stepTwoNodes", stepTwoNodes) + if err != nil { + return errors.WithStack(err) + } + regFlow.InternalContext, err = sjson.SetBytes(regFlow.InternalContext, "stepOneNodes", stepOneNodes) + if err != nil { + return errors.WithStack(err) + } + + return nil +} diff --git a/selfservice/hook/verification.go b/selfservice/hook/verification.go index c98bb91736ac..b66684e5f37c 100644 --- a/selfservice/hook/verification.go +++ b/selfservice/hook/verification.go @@ -12,10 +12,12 @@ import ( "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" "github.com/ory/x/otelx" ) @@ -23,6 +25,7 @@ import ( var ( _ registration.PostHookPostPersistExecutor = new(Verifier) _ settings.PostHookPostPersistExecutor = new(Verifier) + _ login.PostHookExecutor = new(Verifier) ) type ( @@ -34,6 +37,7 @@ type ( verification.FlowPersistenceProvider identity.PrivilegedPoolProvider x.WriterProvider + x.TracingProvider } Verifier struct { r verifierDependencies @@ -61,6 +65,18 @@ func (e *Verifier) ExecuteSettingsPostPersistHook(w http.ResponseWriter, r *http }) } +func (e *Verifier) ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, g node.UiNodeGroup, f *login.Flow, s *session.Session) (err error) { + ctx, span := e.r.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.hook.Verifier.ExecuteLoginPostHook") + r = r.WithContext(ctx) + defer otelx.End(span, &err) + if f.RequestedAAL != identity.AuthenticatorAssuranceLevel1 { + span.AddEvent("Skipping verification hook because AAL is not 1") + return nil + } + + return e.do(w, r.WithContext(ctx), s.Identity, f, nil) +} + func (e *Verifier) do( w http.ResponseWriter, r *http.Request, @@ -78,11 +94,16 @@ func (e *Verifier) do( } isBrowserFlow := f.GetType() == flow.TypeBrowser - isRegistrationFlow := f.GetFlowName() == flow.RegistrationFlow + isRegistrationOrLoginFlow := f.GetFlowName() == flow.RegistrationFlow || f.GetFlowName() == flow.LoginFlow for k := range i.VerifiableAddresses { address := &i.VerifiableAddresses[k] - if address.Status != identity.VerifiableAddressStatusPending { + if isRegistrationOrLoginFlow && address.Verified { + continue + } else if !isRegistrationOrLoginFlow && address.Status != identity.VerifiableAddressStatusPending { + // In case of the settings flow, we only want to create a new verification flow if there is no pending + // verification flow for the address. Otherwise, we would create a new verification flow for each setting, + // even if the address did not change. continue } @@ -90,7 +111,7 @@ func (e *Verifier) do( // TODO: this is pretty ugly, we should probably have a better way to handle CSRF tokens here. if isBrowserFlow { - if isRegistrationFlow { + if isRegistrationOrLoginFlow { // If this hook is executed from a registration flow, we need to regenerate the CSRF token. csrf = e.r.CSRFHandler().RegenerateToken(w, r) } else { diff --git a/selfservice/hook/verification_test.go b/selfservice/hook/verification_test.go index b24290dcec9d..5e815fa4d4a4 100644 --- a/selfservice/hook/verification_test.go +++ b/selfservice/hook/verification_test.go @@ -12,6 +12,7 @@ import ( "github.com/ory/kratos/courier" "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/ui/node" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,11 +21,13 @@ import ( "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/selfservice/hook" "github.com/ory/kratos/session" "github.com/ory/kratos/x" + "github.com/ory/x/pointerx" "github.com/ory/x/sqlxx" "github.com/ory/x/urlx" ) @@ -32,109 +35,185 @@ import ( func TestVerifier(t *testing.T) { ctx := context.Background() u := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")} - for k, hf := range map[string]func(*hook.Verifier, *identity.Identity, flow.Flow) error{ - "settings": func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error { - return h.ExecuteSettingsPostPersistHook( - httptest.NewRecorder(), u, f.(*settings.Flow), i, &session.Session{ID: x.NewUUID(), Identity: i}, "ExecuteSettingsPostPersistHook") + + for _, tc := range []struct { + name string + execHook func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error + originalFlow func() flow.FlowWithContinueWith + }{ + { + name: "login", + execHook: func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error { + return h.ExecuteLoginPostHook( + httptest.NewRecorder(), u, node.CodeGroup, f.(*login.Flow), &session.Session{ID: x.NewUUID(), Identity: i}) + }, + originalFlow: func() flow.FlowWithContinueWith { + return &login.Flow{RequestURL: "http://foo.com/login", RequestedAAL: "aal1"} + }, }, - "register": func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error { - return h.ExecutePostRegistrationPostPersistHook( - httptest.NewRecorder(), u, f.(*registration.Flow), &session.Session{ID: x.NewUUID(), Identity: i}, identity.CredentialsTypePassword) + { + name: "registration", + execHook: func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error { + return h.ExecutePostRegistrationPostPersistHook( + httptest.NewRecorder(), u, f.(*registration.Flow), &session.Session{ID: x.NewUUID(), Identity: i}) + }, + originalFlow: func() flow.FlowWithContinueWith { + return ®istration.Flow{RequestURL: "http://foo.com/registration?after_verification_return_to=verification_callback"} + }, }, } { - t.Run("name="+k, func(t *testing.T) { - conf, reg := internal.NewFastRegistryWithMocks(t) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.schema.json") - conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") - conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") - - i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) - i.Traits = identity.Traits(`{"emails":["foo@ory.sh","bar@ory.sh","baz@ory.sh"]}`) - require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) - - actual, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "foo@ory.sh") - require.NoError(t, err) - assert.EqualValues(t, "foo@ory.sh", actual.Value) - - actual, err = reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "bar@ory.sh") - require.NoError(t, err) - assert.EqualValues(t, "bar@ory.sh", actual.Value) - - actual, err = reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "baz@ory.sh") - require.NoError(t, err) - assert.EqualValues(t, "baz@ory.sh", actual.Value) - - verifiedAt := sqlxx.NullTime(time.Now()) - actual.Status = identity.VerifiableAddressStatusCompleted - actual.Verified = true - actual.VerifiedAt = &verifiedAt - require.NoError(t, reg.PrivilegedIdentityPool().UpdateVerifiableAddress(context.Background(), actual)) - - i, err = reg.IdentityPool().GetIdentity(context.Background(), i.ID, identity.ExpandDefault) - require.NoError(t, err) - - var originalFlow flow.FlowWithContinueWith - switch k { - case "settings": - originalFlow = &settings.Flow{RequestURL: "http://foo.com/settings?after_verification_return_to=verification_callback"} - case "register": - originalFlow = ®istration.Flow{RequestURL: "http://foo.com/registration?after_verification_return_to=verification_callback"} - default: - t.FailNow() - } - - h := hook.NewVerifier(reg) - require.NoError(t, hf(h, i, originalFlow)) - assert.Lenf(t, originalFlow.ContinueWith(), 2, "%#ßv", originalFlow.ContinueWith()) - assertContinueWithAddresses(t, originalFlow.ContinueWith(), []string{"foo@ory.sh", "bar@ory.sh"}) - vf := originalFlow.ContinueWith()[0] - assert.IsType(t, &flow.ContinueWithVerificationUI{}, vf) - fView := vf.(*flow.ContinueWithVerificationUI).Flow - - expectedVerificationFlow, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, fView.ID) - require.NoError(t, err) - require.Equal(t, expectedVerificationFlow.State, flow.StateEmailSent) - - messages, err := reg.CourierPersister().NextMessages(context.Background(), 12) - require.NoError(t, err) - require.Len(t, messages, 2) - - recipients := make([]string, len(messages)) - for k, m := range messages { - recipients[k] = m.Recipient - } - - assert.Contains(t, recipients, "foo@ory.sh") - assert.Contains(t, recipients, "bar@ory.sh") - // Email to baz@ory.sh is skipped because it is verified already. - assert.NotContains(t, recipients, "baz@ory.sh") - - // these addresses will be marked as sent and won't be sent again by the settings hook - address1, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "foo@ory.sh") - require.NoError(t, err) - assert.EqualValues(t, identity.VerifiableAddressStatusSent, address1.Status) - address2, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "bar@ory.sh") - require.NoError(t, err) - assert.EqualValues(t, identity.VerifiableAddressStatusSent, address2.Status) - - switch k { - case "settings": - originalFlow = &settings.Flow{RequestURL: "http://foo.com/settings?after_verification_return_to=verification_callback"} - case "register": - originalFlow = ®istration.Flow{RequestURL: "http://foo.com/registration?after_verification_return_to=verification_callback"} - default: - t.FailNow() - } - require.NoError(t, hf(h, i, originalFlow)) - - assert.Emptyf(t, originalFlow.ContinueWith(), "%+v", originalFlow.ContinueWith()) - - require.NoError(t, err) - messages, err = reg.CourierPersister().NextMessages(context.Background(), 12) - require.EqualError(t, err, courier.ErrQueueEmpty.Error()) - assert.Len(t, messages, 0) + t.Run("flow="+tc.name, func(t *testing.T) { + t.Run("case=should send out emails for unverified addresses", func(t *testing.T) { + t.Parallel() + originalFlow := tc.originalFlow() + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.schema.json") + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") + conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Traits = identity.Traits(`{"emails":["foo@ory.sh","bar@ory.sh"]}`) + require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) + + h := hook.NewVerifier(reg) + require.NoError(t, tc.execHook(h, i, originalFlow)) + assert.Lenf(t, originalFlow.ContinueWith(), 2, "%#ßv", originalFlow.ContinueWith()) + assertContinueWithAddresses(t, originalFlow.ContinueWith(), []string{"foo@ory.sh", "bar@ory.sh"}) + vf := originalFlow.ContinueWith()[0] + assert.IsType(t, &flow.ContinueWithVerificationUI{}, vf) + fView := vf.(*flow.ContinueWithVerificationUI).Flow + + expectedVerificationFlow, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, fView.ID) + require.NoError(t, err) + require.Equal(t, expectedVerificationFlow.State, flow.StateEmailSent) + + messages, err := reg.CourierPersister().NextMessages(context.Background(), 12) + require.NoError(t, err) + require.Len(t, messages, 2) + }) + + t.Run("case should skip already verified addresses", func(t *testing.T) { + t.Parallel() + originalFlow := tc.originalFlow() + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.schema.json") + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") + conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") + h := hook.NewVerifier(reg) + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Traits = identity.Traits(`{"emails":["foo@ory.sh","bar@ory.sh"]}`) + require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) + for _, address := range i.VerifiableAddresses { + address.Verified = true + address.VerifiedAt = pointerx.Ptr(sqlxx.NullTime(time.Now())) + address.Status = identity.VerifiableAddressStatusCompleted + reg.Persister().UpdateVerifiableAddress(context.Background(), &address) + } + i, err := reg.PrivilegedIdentityPool().GetIdentity(ctx, i.ID, identity.ExpandDefault) + require.NoError(t, err) + + require.NoError(t, tc.execHook(h, i, originalFlow)) + assert.Empty(t, originalFlow.ContinueWith(), "%#ßv", originalFlow.ContinueWith()) + }) }) } + + t.Run("flow=login/case=does not run if aal is not 1", func(t *testing.T) { + t.Parallel() + _, reg := internal.NewFastRegistryWithMocks(t) + + h := hook.NewVerifier(reg) + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + f := &login.Flow{RequestedAAL: "aal2"} + require.NoError(t, h.ExecuteLoginPostHook(httptest.NewRecorder(), u, node.CodeGroup, f, &session.Session{ID: x.NewUUID(), Identity: i})) + + messages, err := reg.CourierPersister().NextMessages(context.Background(), 12) + require.EqualError(t, err, "queue is empty") + require.Len(t, messages, 0) + }) + + t.Run("name=register", func(t *testing.T) { + t.Parallel() + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.schema.json") + conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") + conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/") + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.Traits = identity.Traits(`{"emails":["foo@ory.sh","bar@ory.sh","baz@ory.sh"]}`) + require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) + + actual, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "foo@ory.sh") + require.NoError(t, err) + assert.EqualValues(t, "foo@ory.sh", actual.Value) + + actual, err = reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "bar@ory.sh") + require.NoError(t, err) + assert.EqualValues(t, "bar@ory.sh", actual.Value) + + actual, err = reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "baz@ory.sh") + require.NoError(t, err) + assert.EqualValues(t, "baz@ory.sh", actual.Value) + + verifiedAt := sqlxx.NullTime(time.Now()) + actual.Status = identity.VerifiableAddressStatusCompleted + actual.Verified = true + actual.VerifiedAt = &verifiedAt + require.NoError(t, reg.PrivilegedIdentityPool().UpdateVerifiableAddress(context.Background(), actual)) + + i, err = reg.IdentityPool().GetIdentity(context.Background(), i.ID, identity.ExpandDefault) + require.NoError(t, err) + + originalFlow := &settings.Flow{RequestURL: "http://foo.com/settings?after_verification_return_to=verification_callback"} + + h := hook.NewVerifier(reg) + require.NoError(t, h.ExecuteSettingsPostPersistHook( + httptest.NewRecorder(), u, originalFlow, i, &session.Session{ID: x.NewUUID(), Identity: i})) + assert.Lenf(t, originalFlow.ContinueWith(), 2, "%#ßv", originalFlow.ContinueWith()) + assertContinueWithAddresses(t, originalFlow.ContinueWith(), []string{"foo@ory.sh", "bar@ory.sh"}) + vf := originalFlow.ContinueWith()[0] + assert.IsType(t, &flow.ContinueWithVerificationUI{}, vf) + fView := vf.(*flow.ContinueWithVerificationUI).Flow + + expectedVerificationFlow, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, fView.ID) + require.NoError(t, err) + require.Equal(t, expectedVerificationFlow.State, flow.StateEmailSent) + + messages, err := reg.CourierPersister().NextMessages(context.Background(), 12) + require.NoError(t, err) + require.Len(t, messages, 2) + + recipients := make([]string, len(messages)) + for k, m := range messages { + recipients[k] = m.Recipient + } + + assert.Contains(t, recipients, "foo@ory.sh") + assert.Contains(t, recipients, "bar@ory.sh") + // Email to baz@ory.sh is skipped because it is verified already. + assert.NotContains(t, recipients, "baz@ory.sh") + + // these addresses will be marked as sent and won't be sent again by the settings hook + address1, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "foo@ory.sh") + require.NoError(t, err) + assert.EqualValues(t, identity.VerifiableAddressStatusSent, address1.Status) + address2, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "bar@ory.sh") + require.NoError(t, err) + assert.EqualValues(t, identity.VerifiableAddressStatusSent, address2.Status) + + originalFlow = &settings.Flow{RequestURL: "http://foo.com/settings?after_verification_return_to=verification_callback"} + + require.NoError(t, h.ExecuteSettingsPostPersistHook( + httptest.NewRecorder(), u, originalFlow, i, &session.Session{ID: x.NewUUID(), Identity: i})) + + assert.Emptyf(t, originalFlow.ContinueWith(), "%+v", originalFlow.ContinueWith()) + + require.NoError(t, err) + messages, err = reg.CourierPersister().NextMessages(context.Background(), 12) + require.EqualError(t, err, courier.ErrQueueEmpty.Error()) + assert.Len(t, messages, 0) + }) } func assertContinueWithAddresses(t *testing.T, cs []flow.ContinueWith, addresses []string) { diff --git a/selfservice/hook/web_hook.go b/selfservice/hook/web_hook.go index d66651f25edd..1e90fba25b45 100644 --- a/selfservice/hook/web_hook.go +++ b/selfservice/hook/web_hook.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "net/http" + "net/textproto" "net/url" "time" @@ -22,6 +23,7 @@ import ( "go.opentelemetry.io/otel/codes" semconv "go.opentelemetry.io/otel/semconv/v1.11.0" "go.opentelemetry.io/otel/trace" + "golang.org/x/exp/maps" grpccodes "google.golang.org/grpc/codes" "github.com/ory/herodot" @@ -85,6 +87,7 @@ type ( RequestURL string `json:"request_url"` RequestCookies map[string]string `json:"request_cookies"` Identity *identity.Identity `json:"identity,omitempty"` + Session *session.Session `json:"session,omitempty"` // fandom-start Credentials *identity.Credentials `json:"credentials,omitempty"` Fields url.Values `json:"fields,omitempty"` @@ -186,6 +189,7 @@ func (e *WebHook) ExecuteLoginPostHook(_ http.ResponseWriter, req *http.Request, RequestURL: x.RequestURL(req).String(), RequestCookies: cookies(req), Identity: session.Identity, + Session: session, HookType: "LoginPostHook", Fields: req.Form, }) @@ -512,6 +516,8 @@ func (e *WebHook) execute(ctx context.Context, data *templateContext) error { attribute.Bool("webhook.response.parse", parseResponse), ) + removeDisallowedHeaders(data) + req, err := builder.BuildRequest(ctx, data) if errors.Is(err, request.ErrCancel) { span.SetAttributes(attribute.Bool("webhook.jsonnet.canceled", true)) @@ -580,7 +586,38 @@ func (e *WebHook) execute(ctx context.Context, data *templateContext) error { return nil } -func (e *WebHook) parseWebhookResponse(resp *http.Response, id *identity.Identity) (err error) { +// RequestHeaderAllowList contains the allowed request headers that are forwarded +// to the web hook target in canonical form (textproto.CanonicalMIMEHeaderKey). +var RequestHeaderAllowList = map[string]struct{}{ + "Accept": {}, + "Accept-Encoding": {}, + "Accept-Language": {}, + "Content-Length": {}, + "Content-Type": {}, + "Origin": {}, + "Priority": {}, + "Referer": {}, + "Sec-Ch-Ua": {}, + "Sec-Ch-Ua-Mobile": {}, + "Sec-Ch-Ua-Platform": {}, + "Sec-Fetch-Dest": {}, + "Sec-Fetch-Mode": {}, + "Sec-Fetch-Site": {}, + "Sec-Fetch-User": {}, + "True-Client-Ip": {}, + "User-Agent": {}, +} + +func removeDisallowedHeaders(data *templateContext) { + headers := maps.Clone(data.RequestHeaders) + maps.DeleteFunc(headers, func(key string, _ []string) bool { + _, found := RequestHeaderAllowList[textproto.CanonicalMIMEHeaderKey(key)] + return !found + }) + data.RequestHeaders = headers +} + +func parseWebhookResponse(resp *http.Response, id *identity.Identity) (err error) { if resp == nil { return errors.Errorf("empty response provided from the webhook") } diff --git a/selfservice/hook/web_hook_integration_test.go b/selfservice/hook/web_hook_integration_test.go index 1ebc463991f7..aa47f5f6dd99 100644 --- a/selfservice/hook/web_hook_integration_test.go +++ b/selfservice/hook/web_hook_integration_test.go @@ -23,6 +23,7 @@ import ( "github.com/sirupsen/logrus/hooks/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tidwall/sjson" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" "golang.org/x/exp/slices" @@ -52,6 +53,13 @@ import ( "github.com/ory/x/snapshotx" ) +var transientPayload = json.RawMessage(`{ + "stuff": { + "name": "fubar", + "numbers": [42, 12345, 3.1415] + } +}`) + type resilientClientProvider struct { log *logrusx.Logger } @@ -138,10 +146,8 @@ func TestWebHooks(t *testing.T) { } bodyWithFlowOnly := func(req *http.Request, f flow.Flow) string { - h, _ := json.Marshal(req.Header) - return fmt.Sprintf(`{ - "flow_id": "%s", - "headers": %s, + body := fmt.Sprintf(`{ + "flow_id": "%s", "method": "%s", "url": "%s", "cookies": { @@ -149,31 +155,43 @@ func TestWebHooks(t *testing.T) { "Some-Cookie-2": "Some-other-Cookie-Value", "Some-Cookie-3": "Third-Cookie-Value" } - }`, f.GetID(), string(h), req.Method, "http://www.ory.sh/some_end_point") + }`, f.GetID(), req.Method, "http://www.ory.sh/some_end_point") + if len(req.Header) != 0 { + if ua := req.Header.Get("User-Agent"); ua != "" { + body, _ = sjson.Set(body, "headers.User-Agent", []string{ua}) + } + } + + return body } - bodyWithFlowAndIdentity := func(req *http.Request, f flow.Flow, s *session.Session) string { - h, _ := json.Marshal(req.Header) - return fmt.Sprintf(`{ - "flow_id": "%s", + bodyWithFlowAndIdentityAndTransientPayload := func(req *http.Request, f flow.Flow, s *session.Session, tp json.RawMessage) string { + body := fmt.Sprintf(`{ + "flow_id": "%s", "identity_id": "%s", - "headers": %s, "method": "%s", "url": "%s", "cookies": { "Some-Cookie-1": "Some-Cookie-Value", "Some-Cookie-2": "Some-other-Cookie-Value", "Some-Cookie-3": "Third-Cookie-Value" - } - }`, f.GetID(), s.Identity.ID, string(h), req.Method, "http://www.ory.sh/some_end_point") + }, + "transient_payload": %s + }`, f.GetID(), s.Identity.ID, req.Method, "http://www.ory.sh/some_end_point", string(tp)) + if len(req.Header) != 0 { + if ua := req.Header.Get("User-Agent"); ua != "" { + body, _ = sjson.Set(body, "headers.User-Agent", []string{ua}) + } + } + + return body } - bodyWithFlowAndIdentityAndTransientPayload := func(req *http.Request, f flow.Flow, s *session.Session, tp json.RawMessage) string { - h, _ := json.Marshal(req.Header) - return fmt.Sprintf(`{ - "flow_id": "%s", + bodyWithFlowAndIdentityAndSessionAndTransientPayload := func(req *http.Request, f flow.Flow, s *session.Session, tp json.RawMessage) string { + body := fmt.Sprintf(`{ + "flow_id": "%s", "identity_id": "%s", - "headers": %s, + "session_id": "%s", "method": "%s", "url": "%s", "cookies": { @@ -182,7 +200,14 @@ func TestWebHooks(t *testing.T) { "Some-Cookie-3": "Third-Cookie-Value" }, "transient_payload": %s - }`, f.GetID(), s.Identity.ID, string(h), req.Method, "http://www.ory.sh/some_end_point", string(tp)) + }`, f.GetID(), s.Identity.ID, s.ID, req.Method, "http://www.ory.sh/some_end_point", string(tp)) + if len(req.Header) != 0 { + if ua := req.Header.Get("User-Agent"); ua != "" { + body, _ = sjson.Set(body, "headers.User-Agent", []string{ua}) + } + } + + return body } for _, tc := range []struct { @@ -203,12 +228,12 @@ func TestWebHooks(t *testing.T) { }, { uc: "Post Login Hook", - createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} }, - callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session, _ identity.CredentialsType) error { + createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID(), TransientPayload: transientPayload} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { return wh.ExecuteLoginPostHook(nil, req, node.PasswordGroup, f.(*login.Flow), s) }, expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { - return bodyWithFlowAndIdentity(req, f, s) + return bodyWithFlowAndIdentityAndSessionAndTransientPayload(req, f, s, transientPayload) }, }, { @@ -225,55 +250,45 @@ func TestWebHooks(t *testing.T) { uc: "Post Registration Hook", createFlow: func() flow.Flow { return ®istration.Flow{ - ID: x.NewUUID(), - TransientPayload: json.RawMessage(`{ - "stuff": { - "name": "fubar", - "numbers": [42, 12345, 3.1415] - } - }`), + ID: x.NewUUID(), + TransientPayload: transientPayload, } }, callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session, ct identity.CredentialsType) error { return wh.ExecutePostRegistrationPostPersistHook(nil, req, f.(*registration.Flow), s, ct) }, expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { - return bodyWithFlowAndIdentityAndTransientPayload(req, f, s, json.RawMessage(`{ - "stuff": { - "name": "fubar", - "numbers": [42, 12345, 3.1415] - } - }`)) + return bodyWithFlowAndIdentityAndTransientPayload(req, f, s, transientPayload) }, }, { uc: "Post Recovery Hook", - createFlow: func() flow.Flow { return &recovery.Flow{ID: x.NewUUID()} }, - callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session, _ identity.CredentialsType) error { + createFlow: func() flow.Flow { return &recovery.Flow{ID: x.NewUUID(), TransientPayload: transientPayload} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { return wh.ExecutePostRecoveryHook(nil, req, f.(*recovery.Flow), s) }, expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { - return bodyWithFlowAndIdentity(req, f, s) + return bodyWithFlowAndIdentityAndTransientPayload(req, f, s, transientPayload) }, }, { uc: "Post Verification Hook", - createFlow: func() flow.Flow { return &verification.Flow{ID: x.NewUUID()} }, - callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session, _ identity.CredentialsType) error { + createFlow: func() flow.Flow { return &verification.Flow{ID: x.NewUUID(), TransientPayload: transientPayload} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { return wh.ExecutePostVerificationHook(nil, req, f.(*verification.Flow), s.Identity) }, expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { - return bodyWithFlowAndIdentity(req, f, s) + return bodyWithFlowAndIdentityAndTransientPayload(req, f, s, transientPayload) }, }, { uc: "Post Settings Hook", - createFlow: func() flow.Flow { return &settings.Flow{ID: x.NewUUID()} }, - callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session, _ identity.CredentialsType) error { - return wh.ExecuteSettingsPostPersistHook(nil, req, f.(*settings.Flow), s.Identity, s, "PostSettings") + createFlow: func() flow.Flow { return &settings.Flow{ID: x.NewUUID(), TransientPayload: transientPayload} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { + return wh.ExecuteSettingsPostPersistHook(nil, req, f.(*settings.Flow), s.Identity, s) }, expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { - return bodyWithFlowAndIdentity(req, f, s) + return bodyWithFlowAndIdentityAndTransientPayload(req, f, s, transientPayload) }, }, } { @@ -345,8 +360,10 @@ func TestWebHooks(t *testing.T) { req := &http.Request{ Host: "www.ory.sh", Header: map[string][]string{ - "Some-Header": {"Some-Value"}, - "Cookie": {"Some-Cookie-1=Some-Cookie-Value; Some-Cookie-2=Some-other-Cookie-Value", "Some-Cookie-3=Third-Cookie-Value"}, + "Some-Header": {"Some-Value"}, + "User-Agent": {"Foo-Bar-Browser"}, + "Invalid-Header": {"ignored"}, + "Cookie": {"Some-Cookie-1=Some-Cookie-Value; Some-Cookie-2=Some-other-Cookie-Value", "Some-Cookie-3=Third-Cookie-Value"}, }, RequestURI: "/some_end_point", Method: http.MethodPost, diff --git a/selfservice/sessiontokenexchange/test/persistence.go b/selfservice/sessiontokenexchange/test/persistence.go index 53db63db04f3..da19c3edc1a3 100644 --- a/selfservice/sessiontokenexchange/test/persistence.go +++ b/selfservice/sessiontokenexchange/test/persistence.go @@ -11,7 +11,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ory/kratos/driver/config" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" "github.com/ory/kratos/selfservice/sessiontokenexchange" @@ -36,11 +35,10 @@ func (t *testParams) setCodes(e *sessiontokenexchange.Exchanger) { t.returnToCode = e.ReturnToCode } -func TestPersister(ctx context.Context, _ *config.Config, p interface { +func TestPersister(ctx context.Context, p interface { persistence.Persister }) func(t *testing.T) { return func(t *testing.T) { - t.Parallel() nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) t.Run("suite=create-update-get", func(t *testing.T) { diff --git a/selfservice/strategy/code/.schema/login.schema.json b/selfservice/strategy/code/.schema/login.schema.json index fa030cbd67a0..22286b039149 100644 --- a/selfservice/strategy/code/.schema/login.schema.json +++ b/selfservice/strategy/code/.schema/login.schema.json @@ -4,10 +4,7 @@ "type": "object", "properties": { "method": { - "type": "string", - "enum": [ - "code" - ] + "type": "string" }, "code": { "type": "string" @@ -15,6 +12,9 @@ "identifier": { "type": "string" }, + "address": { + "type": "string" + }, "resend": { "type": "string", "enum": [ @@ -27,6 +27,10 @@ }, "csrf_token": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/code/.schema/recovery.schema.json b/selfservice/strategy/code/.schema/recovery.schema.json index 37b6f11e7f06..eb04bd359002 100644 --- a/selfservice/strategy/code/.schema/recovery.schema.json +++ b/selfservice/strategy/code/.schema/recovery.schema.json @@ -4,11 +4,7 @@ "type": "object", "properties": { "method": { - "type": "string", - "enum": [ - "code", - "link" - ] + "type": "string" }, "code": { "type": "string" @@ -23,6 +19,10 @@ }, "csrf_token": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/code/.schema/registration.schema.json b/selfservice/strategy/code/.schema/registration.schema.json index 90f245c107c5..0cd5282eb9ee 100644 --- a/selfservice/strategy/code/.schema/registration.schema.json +++ b/selfservice/strategy/code/.schema/registration.schema.json @@ -4,10 +4,7 @@ "type": "object", "properties": { "method": { - "type": "string", - "enum": [ - "code" - ] + "type": "string" }, "csrf_token": { "type": "string" diff --git a/selfservice/strategy/code/.schema/verification.schema.json b/selfservice/strategy/code/.schema/verification.schema.json index 107d331972b1..e60989dcf52e 100644 --- a/selfservice/strategy/code/.schema/verification.schema.json +++ b/selfservice/strategy/code/.schema/verification.schema.json @@ -23,6 +23,10 @@ }, "csrf_token": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json b/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json index a9f46bedb5c4..736578d0e543 100644 --- a/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json +++ b/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json @@ -6,7 +6,9 @@ "name": "code", "type": "text", "required": true, + "pattern": "[0-9]+", "disabled": false, + "maxlength": 6, "node_type": "input" }, "messages": [], @@ -31,8 +33,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/.snapshots/TestAdminStrategy-description=should_fail_on_negative_expiry_time.json b/selfservice/strategy/code/.snapshots/TestAdminStrategy-description=should_fail_on_negative_expiry_time.json index 0ebac1807073..e9751eea8e8f 100644 --- a/selfservice/strategy/code/.snapshots/TestAdminStrategy-description=should_fail_on_negative_expiry_time.json +++ b/selfservice/strategy/code/.snapshots/TestAdminStrategy-description=should_fail_on_negative_expiry_time.json @@ -2,7 +2,7 @@ "error": { "code": 400, "message": "The request was malformed or contained invalid parameters", - "reason": "Value from \"expires_in\" must result to a future time: -1h", + "reason": "Value from \"expires_in\" must result to a future time: -1h0m0s", "status": "Bad Request" } } diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=code_is_used_for_2fa_but_request_is_1fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=code_is_used_for_2fa_but_request_is_1fa.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=code_is_used_for_2fa_but_request_is_1fa.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=code_is_used_for_passwordless_login_and_request_is_1fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=code_is_used_for_passwordless_login_and_request_is_1fa.json new file mode 100644 index 000000000000..8e9874d00cbf --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=code_is_used_for_passwordless_login_and_request_is_1fa.json @@ -0,0 +1,54 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "text", + "value": "", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010015, + "text": "Send sign in code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh-case=code_is_used_for_2fa_and_request_is_1fa_with_refresh.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh-case=code_is_used_for_2fa_and_request_is_1fa_with_refresh.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh-case=code_is_used_for_2fa_and_request_is_1fa_with_refresh.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh-case=code_is_used_for_passwordless_login_and_request_is_1fa_with_refresh.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh-case=code_is_used_for_passwordless_login_and_request_is_1fa_with_refresh.json new file mode 100644 index 000000000000..8e9874d00cbf --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh-case=code_is_used_for_passwordless_login_and_request_is_1fa_with_refresh.json @@ -0,0 +1,54 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "text", + "value": "", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010015, + "text": "Send sign in code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_code_method-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_code_method-case=code_is_used_for_2fa.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_code_method-case=code_is_used_for_2fa.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_code_method-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_code_method-case=code_is_used_for_passwordless_login.json new file mode 100644 index 000000000000..66b84bf1a436 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_code_method-case=code_is_used_for_passwordless_login.json @@ -0,0 +1,21 @@ +[ + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010015, + "text": "Send sign in code", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_code_method-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_code_method-case=code_is_used_for_2fa.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_code_method-case=code_is_used_for_2fa.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_code_method-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_code_method-case=code_is_used_for_passwordless_login.json new file mode 100644 index 000000000000..66b84bf1a436 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_code_method-case=code_is_used_for_passwordless_login.json @@ -0,0 +1,21 @@ +[ + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010015, + "text": "Send sign in code", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=with_no_identity-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=with_no_identity-case=code_is_used_for_2fa.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=with_no_identity-case=code_is_used_for_2fa.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=with_no_identity-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=with_no_identity-case=code_is_used_for_passwordless_login.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=with_no_identity-case=code_is_used_for_passwordless_login.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=code_is_used_for_2fa.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=code_is_used_for_2fa.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=code_is_used_for_passwordless_login.json new file mode 100644 index 000000000000..66b84bf1a436 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=code_is_used_for_passwordless_login.json @@ -0,0 +1,21 @@ +[ + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010015, + "text": "Send sign in code", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=code_is_used_for_2fa.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=code_is_used_for_2fa.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=code_is_used_for_passwordless_login.json new file mode 100644 index 000000000000..8e9874d00cbf --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=code_is_used_for_passwordless_login.json @@ -0,0 +1,54 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "text", + "value": "", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "method", + "type": "submit", + "value": "code", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010015, + "text": "Send sign in code", + "type": "info" + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_2fa_and_request_is_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_2fa_and_request_is_2fa.json new file mode 100644 index 000000000000..364b8abc331c --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_2fa_and_request_is_2fa.json @@ -0,0 +1,15 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_passwordless_login_and_request_is_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_passwordless_login_and_request_is_2fa.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_passwordless_login_and_request_is_2fa.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-using_via-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-using_via-case=code_is_used_for_2fa.json new file mode 100644 index 000000000000..0680486de62c --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-using_via-case=code_is_used_for_2fa.json @@ -0,0 +1,38 @@ +[ + { + "type": "input", + "group": "code", + "attributes": { + "name": "address", + "type": "submit", + "value": "populateloginmethodsecondfactor-code-mfa-via-2fa@ory.sh", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010023, + "text": "Send code to populateloginmethodsecondfactor-code-mfa-via-2fa@ory.sh", + "type": "info", + "context": { + "address": "populateloginmethodsecondfactor-code-mfa-via-2fa@ory.sh", + "channel": "email" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-using_via-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-using_via-case=code_is_used_for_passwordless_login.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-using_via-case=code_is_used_for_passwordless_login.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-without_via-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-without_via-case=code_is_used_for_2fa.json new file mode 100644 index 000000000000..d050a31ef32d --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-without_via-case=code_is_used_for_2fa.json @@ -0,0 +1,84 @@ +[ + { + "type": "input", + "group": "code", + "attributes": { + "name": "address", + "type": "submit", + "value": "+4917655138291", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010023, + "text": "Send code to +4917655138291", + "type": "info", + "context": { + "address": "+4917655138291", + "channel": "sms" + } + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "address", + "type": "submit", + "value": "populateloginmethodsecondfactor-no-via-2fa-0@ory.sh", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010023, + "text": "Send code to populateloginmethodsecondfactor-no-via-2fa-0@ory.sh", + "type": "info", + "context": { + "address": "populateloginmethodsecondfactor-no-via-2fa-0@ory.sh", + "channel": "email" + } + } + } + }, + { + "type": "input", + "group": "code", + "attributes": { + "name": "address", + "type": "submit", + "value": "populateloginmethodsecondfactor-no-via-2fa-1@ory.sh", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010023, + "text": "Send code to populateloginmethodsecondfactor-no-via-2fa-1@ory.sh", + "type": "info", + "context": { + "address": "populateloginmethodsecondfactor-no-via-2fa-1@ory.sh", + "channel": "email" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-without_via-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-without_via-case=code_is_used_for_passwordless_login.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-without_via-case=code_is_used_for_passwordless_login.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh-case=code_is_used_for_2fa_and_request_is_2fa_with_refresh.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh-case=code_is_used_for_2fa_and_request_is_2fa_with_refresh.json new file mode 100644 index 000000000000..364b8abc331c --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh-case=code_is_used_for_2fa_and_request_is_2fa_with_refresh.json @@ -0,0 +1,15 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh-case=code_is_used_for_passwordless_login_and_request_is_2fa_with_refresh.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh-case=code_is_used_for_passwordless_login_and_request_is_2fa_with_refresh.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh-case=code_is_used_for_passwordless_login_and_request_is_2fa_with_refresh.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=verify_initial_payload.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=verify_initial_payload.json index 14efb22f25a3..612c5c980dc2 100644 --- a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=verify_initial_payload.json +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=verify_initial_payload.json @@ -30,49 +30,21 @@ { "attributes": { "disabled": false, - "name": "identifier", - "node_type": "input", - "required": true, - "type": "text", - "value": "" - }, - "group": "default", - "messages": [ - { - "context": { - "masked_to": "fi****@ory.sh" - }, - "id": 1010020, - "text": "We will send a code to fi****@ory.sh. To verify that this is your address please enter it here.", - "type": "info" - } - ], - "meta": { - "label": { - "context": { - "title": "Email" - }, - "id": 1070002, - "text": "Email", - "type": "info" - } - }, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "method", + "name": "address", "node_type": "input", "type": "submit", - "value": "code" + "value": "fixed_mfa_test_browser@ory.sh" }, "group": "code", "messages": [], "meta": { "label": { - "id": 1010019, - "text": "Continue with code", + "context": { + "address": "fixed_mfa_test_browser@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to fixed_mfa_test_browser@ory.sh", "type": "info" } }, diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=address-email.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=address-email.json new file mode 100644 index 000000000000..1d8d8153054e --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=address-email.json @@ -0,0 +1,71 @@ +[ + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "+4917613213110" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "+4917613213110", + "channel": "sms" + }, + "id": 1010023, + "text": "Send code to +4917613213110", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "code-mfa-1browser@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "code-mfa-1browser@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to code-mfa-1browser@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "code-mfa-2browser@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "code-mfa-2browser@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to code-mfa-2browser@ory.sh", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=address-phone.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=address-phone.json new file mode 100644 index 000000000000..1d8d8153054e --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=address-phone.json @@ -0,0 +1,71 @@ +[ + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "+4917613213110" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "+4917613213110", + "channel": "sms" + }, + "id": 1010023, + "text": "Send code to +4917613213110", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "code-mfa-1browser@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "code-mfa-1browser@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to code-mfa-1browser@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "code-mfa-2browser@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "code-mfa-2browser@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to code-mfa-2browser@ory.sh", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=identifier-email.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=identifier-email.json new file mode 100644 index 000000000000..1d8d8153054e --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Browser_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=identifier-email.json @@ -0,0 +1,71 @@ +[ + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "+4917613213110" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "+4917613213110", + "channel": "sms" + }, + "id": 1010023, + "text": "Send code to +4917613213110", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "code-mfa-1browser@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "code-mfa-1browser@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to code-mfa-1browser@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "code-mfa-2browser@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "code-mfa-2browser@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to code-mfa-2browser@ory.sh", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=verify_initial_payload.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=verify_initial_payload.json index 49f7d3a37cc1..b2dfa774866c 100644 --- a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=verify_initial_payload.json +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=verify_initial_payload.json @@ -17,48 +17,20 @@ { "attributes": { "disabled": false, - "name": "identifier", + "name": "address", "node_type": "input", - "required": true, - "type": "text" - }, - "group": "default", - "messages": [ - { - "context": { - "masked_to": "fi****@ory.sh" - }, - "id": 1010020, - "text": "We will send a code to fi****@ory.sh. To verify that this is your address please enter it here.", - "type": "info" - } - ], - "meta": { - "label": { - "context": { - "title": "Email" - }, - "id": 1070002, - "text": "Email", - "type": "info" - } - }, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "method", - "node_type": "input", - "type": "submit", - "value": "code" + "type": "submit" }, "group": "code", "messages": [], "meta": { "label": { - "id": 1010019, - "text": "Continue with code", + "context": { + "address": "fixed_mfa_test_api@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to fixed_mfa_test_api@ory.sh", "type": "info" } }, diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=address-email.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=address-email.json new file mode 100644 index 000000000000..e3510d16393a --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=address-email.json @@ -0,0 +1,71 @@ +[ + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "+4917613213111" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "+4917613213111", + "channel": "sms" + }, + "id": 1010023, + "text": "Send code to +4917613213111", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "code-mfa-1api@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "code-mfa-1api@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to code-mfa-1api@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "code-mfa-2api@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "code-mfa-2api@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to code-mfa-2api@ory.sh", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=address-phone.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=address-phone.json new file mode 100644 index 000000000000..e3510d16393a --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=address-phone.json @@ -0,0 +1,71 @@ +[ + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "+4917613213111" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "+4917613213111", + "channel": "sms" + }, + "id": 1010023, + "text": "Send code to +4917613213111", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "code-mfa-1api@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "code-mfa-1api@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to code-mfa-1api@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "code-mfa-2api@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "code-mfa-2api@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to code-mfa-2api@ory.sh", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=identifier-email.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=identifier-email.json new file mode 100644 index 000000000000..e3510d16393a --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=Native_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=identifier-email.json @@ -0,0 +1,71 @@ +[ + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "+4917613213111" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "+4917613213111", + "channel": "sms" + }, + "id": 1010023, + "text": "Send code to +4917613213111", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "code-mfa-1api@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "code-mfa-1api@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to code-mfa-1api@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "code-mfa-2api@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "code-mfa-2api@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to code-mfa-2api@ory.sh", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=verify_initial_payload.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=verify_initial_payload.json index 14efb22f25a3..41c14e29d43d 100644 --- a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=verify_initial_payload.json +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=verify_initial_payload.json @@ -30,49 +30,21 @@ { "attributes": { "disabled": false, - "name": "identifier", - "node_type": "input", - "required": true, - "type": "text", - "value": "" - }, - "group": "default", - "messages": [ - { - "context": { - "masked_to": "fi****@ory.sh" - }, - "id": 1010020, - "text": "We will send a code to fi****@ory.sh. To verify that this is your address please enter it here.", - "type": "info" - } - ], - "meta": { - "label": { - "context": { - "title": "Email" - }, - "id": 1070002, - "text": "Email", - "type": "info" - } - }, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "method", + "name": "address", "node_type": "input", "type": "submit", - "value": "code" + "value": "fixed_mfa_test_spa@ory.sh" }, "group": "code", "messages": [], "meta": { "label": { - "id": 1010019, - "text": "Continue with code", + "context": { + "address": "fixed_mfa_test_spa@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to fixed_mfa_test_spa@ory.sh", "type": "info" } }, diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=address-email.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=address-email.json new file mode 100644 index 000000000000..721c86a79617 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=address-email.json @@ -0,0 +1,71 @@ +[ + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "+4917613213112" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "+4917613213112", + "channel": "sms" + }, + "id": 1010023, + "text": "Send code to +4917613213112", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "code-mfa-1spa@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "code-mfa-1spa@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to code-mfa-1spa@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "code-mfa-2spa@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "code-mfa-2spa@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to code-mfa-2spa@ory.sh", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=address-phone.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=address-phone.json new file mode 100644 index 000000000000..721c86a79617 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=address-phone.json @@ -0,0 +1,71 @@ +[ + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "+4917613213112" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "+4917613213112", + "channel": "sms" + }, + "id": 1010023, + "text": "Send code to +4917613213112", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "code-mfa-1spa@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "code-mfa-1spa@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to code-mfa-1spa@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "code-mfa-2spa@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "code-mfa-2spa@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to code-mfa-2spa@ory.sh", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=identifier-email.json b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=identifier-email.json new file mode 100644 index 000000000000..721c86a79617 --- /dev/null +++ b/selfservice/strategy/code/.snapshots/TestLoginCodeStrategy-test=SPA_client-suite=mfa-case=without_via_parameter_all_options_are_shown-field=identifier-email.json @@ -0,0 +1,71 @@ +[ + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "+4917613213112" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "+4917613213112", + "channel": "sms" + }, + "id": 1010023, + "text": "Send code to +4917613213112", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "code-mfa-1spa@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "code-mfa-1spa@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to code-mfa-1spa@ory.sh", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "address", + "node_type": "input", + "type": "submit", + "value": "code-mfa-2spa@ory.sh" + }, + "group": "code", + "messages": [], + "meta": { + "label": { + "context": { + "address": "code-mfa-2spa@ory.sh", + "channel": "email" + }, + "id": 1010023, + "text": "Send code to code-mfa-2spa@ory.sh", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json index ec1092ad77a6..195ca691e981 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json @@ -43,8 +43,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json index dbf1dcd2cbb7..a5ab6784616a 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json @@ -21,6 +21,7 @@ "required": true, "pattern": "[0-9]+", "disabled": false, + "maxlength": 6, "node_type": "input" }, "messages": [], @@ -58,8 +59,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json index ec1092ad77a6..195ca691e981 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json @@ -43,8 +43,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json index ec1092ad77a6..195ca691e981 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json @@ -43,8 +43,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json index ec1092ad77a6..195ca691e981 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json @@ -43,8 +43,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json index dbf1dcd2cbb7..a5ab6784616a 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json @@ -21,6 +21,7 @@ "required": true, "pattern": "[0-9]+", "disabled": false, + "maxlength": 6, "node_type": "input" }, "messages": [], @@ -58,8 +59,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json index dbf1dcd2cbb7..a5ab6784616a 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json @@ -21,6 +21,7 @@ "required": true, "pattern": "[0-9]+", "disabled": false, + "maxlength": 6, "node_type": "input" }, "messages": [], @@ -58,8 +59,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json index dbf1dcd2cbb7..a5ab6784616a 100644 --- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json +++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json @@ -21,6 +21,7 @@ "required": true, "pattern": "[0-9]+", "disabled": false, + "maxlength": 6, "node_type": "input" }, "messages": [], @@ -58,8 +59,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json index 37f61ac9e827..01def57fd58f 100644 --- a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json +++ b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json @@ -30,8 +30,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json index 42456da54dc5..7e7096cd7358 100644 --- a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json +++ b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json @@ -44,8 +44,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/code/code_login.go b/selfservice/strategy/code/code_login.go index 689d52f0cb4f..820a1818a140 100644 --- a/selfservice/strategy/code/code_login.go +++ b/selfservice/strategy/code/code_login.go @@ -32,7 +32,7 @@ type LoginCode struct { // AddressType represents the type of the address // this can be an email address or a phone number. - AddressType identity.CodeAddressType `json:"-" db:"address_type"` + AddressType identity.CodeChannel `json:"-" db:"address_type"` // CodeHMAC represents the HMACed value of the verification code CodeHMAC string `json:"-" db:"code"` @@ -94,7 +94,7 @@ type CreateLoginCodeParams struct { // AddressType is the type of the address (email or phone number). // required: true - AddressType identity.CodeAddressType + AddressType identity.CodeChannel // Code represents the recovery code // required: true diff --git a/selfservice/strategy/code/code_registration.go b/selfservice/strategy/code/code_registration.go index d8691b54b224..4015474112dd 100644 --- a/selfservice/strategy/code/code_registration.go +++ b/selfservice/strategy/code/code_registration.go @@ -32,7 +32,7 @@ type RegistrationCode struct { // AddressType represents the type of the address // this can be an email address or a phone number. - AddressType identity.CodeAddressType `json:"-" db:"address_type"` + AddressType identity.CodeChannel `json:"-" db:"address_type"` // CodeHMAC represents the HMACed value of the verification code CodeHMAC string `json:"-" db:"code"` @@ -60,7 +60,7 @@ type RegistrationCode struct { NID uuid.UUID `json:"-" faker:"-" db:"nid"` } -func (RegistrationCode) TableName(ctx context.Context) string { +func (RegistrationCode) TableName(context.Context) string { return "identity_registration_codes" } @@ -93,7 +93,7 @@ type CreateRegistrationCodeParams struct { // AddressType is the type of the address (email or phone number). // required: true - AddressType identity.CodeAddressType + AddressType identity.CodeChannel // Code represents the recovery code // required: true diff --git a/selfservice/strategy/code/code_sender.go b/selfservice/strategy/code/code_sender.go index 479dfaad09bc..02fda31f3d0a 100644 --- a/selfservice/strategy/code/code_sender.go +++ b/selfservice/strategy/code/code_sender.go @@ -54,7 +54,7 @@ type ( } Address struct { To string - Via identity.CodeAddressType + Via identity.CodeChannel } ) @@ -69,6 +69,11 @@ func (s *Sender) SendCode(ctx context.Context, f flow.Flow, id *identity.Identit WithSensitiveField("address", addresses). Debugf("Preparing %s code", f.GetFlowName()) + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } + // send to all addresses for _, address := range addresses { // We have to generate a unique code per address, or otherwise it is not possible to link which @@ -82,7 +87,7 @@ func (s *Sender) SendCode(ctx context.Context, f flow.Flow, id *identity.Identit code, err := s.deps. RegistrationCodePersister(). CreateRegistrationCode(ctx, &CreateRegistrationCodeParams{ - AddressType: identity.CodeAddressType(address.Via), + AddressType: address.Via, RawCode: rawCode, ExpiresIn: s.deps.Config().SelfServiceCodeMethodLifespan(ctx), FlowID: f.GetID(), @@ -101,6 +106,7 @@ func (s *Sender) SendCode(ctx context.Context, f flow.Flow, id *identity.Identit RegistrationCode: rawCode, Traits: model, RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, } s.deps.Audit(). @@ -117,7 +123,7 @@ func (s *Sender) SendCode(ctx context.Context, f flow.Flow, id *identity.Identit code, err := s.deps. LoginCodePersister(). CreateLoginCode(ctx, &CreateLoginCodeParams{ - AddressType: identity.CodeAddressType(address.Via), + AddressType: address.Via, Address: address.To, RawCode: rawCode, ExpiresIn: s.deps.Config().SelfServiceCodeMethodLifespan(ctx), @@ -142,17 +148,19 @@ func (s *Sender) SendCode(ctx context.Context, f flow.Flow, id *identity.Identit switch address.Via { case identity.ChannelTypeEmail: t = email.NewLoginCodeValid(s.deps, &email.LoginCodeValidModel{ - To: address.To, - LoginCode: rawCode, - Identity: model, - RequestURL: f.GetRequestURL(), + To: address.To, + LoginCode: rawCode, + Identity: model, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, }) case identity.ChannelTypeSMS: t = sms.NewLoginCodeValid(s.deps, &sms.LoginCodeValidModel{ - To: address.To, - LoginCode: rawCode, - Identity: model, - RequestURL: f.GetRequestURL(), + To: address.To, + LoginCode: rawCode, + Identity: model, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, }) } @@ -188,11 +196,17 @@ func (s *Sender) SendRecoveryCode(ctx context.Context, f *recovery.Flow, via ide WithField("strategy", "code"). WithField("was_notified", notifyUnknownRecipients). Info("Account recovery was requested for an unknown address.") + + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } if !notifyUnknownRecipients { // do nothing } else if err := s.send(ctx, string(via), email.NewRecoveryCodeInvalid(s.deps, &email.RecoveryCodeInvalidModel{ - To: to, - RequestURL: f.RequestURL, + To: to, + RequestURL: f.RequestURL, + TransientPayload: transientPayload, })); err != nil { return err } @@ -227,7 +241,7 @@ func (s *Sender) SendRecoveryCode(ctx context.Context, f *recovery.Flow, via ide return s.SendRecoveryCodeTo(ctx, i, rawCode, code, f) } -func (s *Sender) SendRecoveryCodeTo(ctx context.Context, i *identity.Identity, codeString string, code *RecoveryCode, f flow.Flow) error { +func (s *Sender) SendRecoveryCodeTo(ctx context.Context, i *identity.Identity, codeString string, code *RecoveryCode, f *recovery.Flow) error { s.deps.Audit(). WithField("via", code.RecoveryAddress.Via). WithField("identity_id", code.RecoveryAddress.IdentityID). @@ -241,11 +255,17 @@ func (s *Sender) SendRecoveryCodeTo(ctx context.Context, i *identity.Identity, c return err } + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } + emailModel := email.RecoveryCodeValidModel{ - To: code.RecoveryAddress.Value, - RecoveryCode: codeString, - Identity: model, - RequestURL: f.GetRequestURL(), + To: code.RecoveryAddress.Value, + RecoveryCode: codeString, + Identity: model, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, } return s.send(ctx, string(code.RecoveryAddress.Via), email.NewRecoveryCodeValid(s.deps, &emailModel)) @@ -271,11 +291,17 @@ func (s *Sender) SendVerificationCode(ctx context.Context, f *verification.Flow, WithSensitiveField("email_address", to). WithField("was_notified", notifyUnknownRecipients). Info("Address verification was requested for an unknown address.") + + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } if !notifyUnknownRecipients { // do nothing } else if err := s.send(ctx, string(via), email.NewVerificationCodeInvalid(s.deps, &email.VerificationCodeInvalidModel{ - To: to, - RequestURL: f.GetRequestURL(), + To: to, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, })); err != nil { return err } @@ -302,10 +328,7 @@ func (s *Sender) SendVerificationCode(ctx context.Context, f *verification.Flow, return err } - if err := s.SendVerificationCodeTo(ctx, f, i, rawCode, code); err != nil { - return err - } - return nil + return s.SendVerificationCodeTo(ctx, f, i, rawCode, code) } func (s *Sender) constructVerificationLink(ctx context.Context, fID uuid.UUID, codeStr string) string { @@ -331,6 +354,11 @@ func (s *Sender) SendVerificationCodeTo(ctx context.Context, f *verification.Flo return err } + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } + var t courier.Template // TODO: this can likely be abstracted by making templates not specific to the channel they're using @@ -342,6 +370,7 @@ func (s *Sender) SendVerificationCodeTo(ctx context.Context, f *verification.Flo Identity: model, VerificationCode: codeString, RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, }) case identity.ChannelTypeSMS: t = sms.NewVerificationCodeValid(s.deps, &sms.VerificationCodeValidModel{ @@ -349,6 +378,7 @@ func (s *Sender) SendVerificationCodeTo(ctx context.Context, f *verification.Flo VerificationCode: codeString, Identity: model, RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, }) default: return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected email or sms but got %s", code.VerifiableAddress.Via)) diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go index a4b52e980754..1e8fb5cb6af4 100644 --- a/selfservice/strategy/code/strategy.go +++ b/selfservice/strategy/code/strategy.go @@ -6,8 +6,11 @@ package code import ( "context" "net/http" + "sort" "strings" + "github.com/samber/lo" + "github.com/pkg/errors" "github.com/tidwall/gjson" @@ -36,20 +39,21 @@ import ( ) var ( - _ recovery.Strategy = new(Strategy) - _ recovery.AdminHandler = new(Strategy) - _ recovery.PublicHandler = new(Strategy) + _ recovery.Strategy = (*Strategy)(nil) + _ recovery.AdminHandler = (*Strategy)(nil) + _ recovery.PublicHandler = (*Strategy)(nil) ) var ( - _ verification.Strategy = new(Strategy) - _ verification.AdminHandler = new(Strategy) - _ verification.PublicHandler = new(Strategy) + _ verification.Strategy = (*Strategy)(nil) + _ verification.AdminHandler = (*Strategy)(nil) + _ verification.PublicHandler = (*Strategy)(nil) ) var ( - _ login.Strategy = new(Strategy) - _ registration.Strategy = new(Strategy) + _ login.Strategy = (*Strategy)(nil) + _ registration.Strategy = (*Strategy)(nil) + _ identity.ActiveCredentialsCounter = (*Strategy)(nil) ) type ( @@ -103,7 +107,7 @@ type ( RegistrationCodePersistenceProvider LoginCodePersistenceProvider - schema.IdentityTraitsProvider + schema.IdentitySchemaProvider session.PersistenceProvider sessiontokenexchange.PersistenceProvider @@ -121,6 +125,25 @@ type ( } ) +func (s *Strategy) CountActiveFirstFactorCredentials(ctx context.Context, cc map[identity.CredentialsType]identity.Credentials) (int, error) { + codeConfig := s.deps.Config().SelfServiceCodeStrategy(ctx) + if codeConfig.PasswordlessEnabled { + // Login with code for passwordless is enabled + return 1, nil + } + + return 0, nil +} + +func (s *Strategy) CountActiveMultiFactorCredentials(ctx context.Context, cc map[identity.CredentialsType]identity.Credentials) (int, error) { + codeConfig := s.deps.Config().SelfServiceCodeStrategy(ctx) + if codeConfig.MFAEnabled { + return 1, nil + } + + return 0, nil +} + func NewStrategy(deps any) *Strategy { return &Strategy{deps: deps.(strategyDependencies), dx: decoderx.NewHTTP()} } @@ -180,19 +203,22 @@ func (s *Strategy) PopulateMethod(r *http.Request, f flow.Flow) error { if f.GetType() == flow.TypeBrowser { f.GetUI().SetCSRF(s.deps.GenerateCSRFToken(r)) } + return nil } func (s *Strategy) populateChooseMethodFlow(r *http.Request, f flow.Flow) error { ctx := r.Context() - var codeMetaLabel *text.Message switch f := f.(type) { case *recovery.Flow, *verification.Flow: f.GetUI().Nodes.Append( node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). WithMetaLabel(text.NewInfoNodeInputEmail()), ) - codeMetaLabel = text.NewInfoNodeLabelSubmit() + f.GetUI().Nodes.Append( + node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoNodeLabelContinue()), + ) case *login.Flow: ds, err := s.deps.Config().DefaultIdentityTraitsSchemaURL(ctx) if err != nil { @@ -200,48 +226,80 @@ func (s *Strategy) populateChooseMethodFlow(r *http.Request, f flow.Flow) error } if f.RequestedAAL == identity.AuthenticatorAssuranceLevel2 { via := r.URL.Query().Get("via") - if via == "" { - return errors.WithStack(herodot.ErrBadRequest.WithReason("AAL2 login via code requires the `via` query parameter")) - } sess, err := s.deps.SessionManager().FetchFromRequest(r.Context(), r) if err != nil { return err } - allSchemas, err := s.deps.IdentityTraitsSchemas(ctx) - if err != nil { - return err - } - iSchema, err := allSchemas.GetByID(sess.Identity.SchemaID) - if err != nil { - return err - } - identifierLabel, err := login.GetIdentifierLabelFromSchemaWithField(ctx, iSchema.RawURL, via) - if err != nil { - return err + // We need to load the identity's credentials. + if len(sess.Identity.Credentials) == 0 { + if err := s.deps.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, sess.Identity, identity.ExpandCredentials); err != nil { + return err + } } - value := gjson.GetBytes(sess.Identity.Traits, via).String() - if value == "" { - return errors.WithStack(herodot.ErrBadRequest.WithReasonf("No value found for trait %s in the current identity", via)) - } + // The via parameter lets us hint at the OTP address to use for 2fa. + if via == "" { + addresses, found, err := FindCodeAddressCandidates(sess.Identity, s.deps.Config().SelfServiceCodeMethodMissingCredentialFallbackEnabled(ctx)) + if err != nil { + return err + } else if !found { + return nil + } + + sort.SliceStable(addresses, func(i, j int) bool { + return addresses[i].To < addresses[j].To && addresses[i].Via < addresses[j].Via + }) - codeMetaLabel = text.NewInfoSelfServiceLoginCodeMFA() - idNode := node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel) - idNode.Messages.Add(text.NewInfoSelfServiceLoginCodeMFAHint(MaskAddress(value))) - f.GetUI().Nodes.Upsert(idNode) + for _, address := range addresses { + f.GetUI().Nodes.Append(node.NewInputField("address", address.To, node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoSelfServiceLoginAAL2CodeAddress(string(address.Via), address.To))) + } + } else { + value := gjson.GetBytes(sess.Identity.Traits, via).String() + if value == "" { + return errors.WithStack(herodot.ErrBadRequest.WithReasonf("No value found for trait %s in the current identity.", via)) + } + + // TODO Remove this normalization once the via parameter is deprecated. + // + // Here we need to normalize the via parameter to the actual address. This is necessary because otherwise + // we won't find the address in the list of addresses. + // + // Since we don't know if the via parameter is an email address or a phone number, we need to normalize for both. + value = x.GracefulNormalization(value) + + addresses, found, err := FindCodeAddressCandidates(sess.Identity, s.deps.Config().SelfServiceCodeMethodMissingCredentialFallbackEnabled(ctx)) + if err != nil { + return err + } else if !found { + return nil + } + + address, found := lo.Find(addresses, func(item Address) bool { + return item.To == value + }) + if !found { + return errors.WithStack(herodot.ErrBadRequest.WithReasonf("You can only reference a trait that matches a verification email address in the via parameter, or a registered credential.")) + } + + f.GetUI().Nodes.Append(node.NewInputField("address", address.To, node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoSelfServiceLoginAAL2CodeAddress(string(address.Via), address.To))) + } } else { - codeMetaLabel = text.NewInfoSelfServiceLoginCode() identifierLabel, err := login.GetIdentifierLabelFromSchema(ctx, ds.String()) if err != nil { return err } f.GetUI().Nodes.Upsert(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel)) + f.GetUI().Nodes.Append( + node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceLoginCode()), + ) } + case *registration.Flow: - codeMetaLabel = text.NewInfoSelfServiceRegistrationRegisterCode() ds, err := s.deps.Config().DefaultIdentityTraitsSchemaURL(ctx) if err != nil { return err @@ -257,12 +315,12 @@ func (s *Strategy) populateChooseMethodFlow(r *http.Request, f flow.Flow) error for _, n := range traitNodes { f.GetUI().Nodes.Upsert(n) } - } - methodButton := node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit). - WithMetaLabel(codeMetaLabel) - - f.GetUI().Nodes.Append(methodButton) + f.GetUI().Nodes.Append( + node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit). + WithMetaLabel(text.NewInfoSelfServiceRegistrationRegisterCode()), + ) + } return nil } @@ -275,6 +333,7 @@ func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error var message *text.Message var resendNode *node.Node + var backNode *node.Node switch f.GetFlowName() { case flow.RecoveryFlow: @@ -284,6 +343,7 @@ func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error resendNode = node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). WithMetaLabel(text.NewInfoNodeResendOTP()) + case flow.VerificationFlow: route = verification.RouteSubmitFlow codeMetaLabel = text.NewInfoNodeLabelVerificationCode() @@ -297,15 +357,11 @@ func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error // preserve the login identifier that was submitted // so we can retry the code flow with the same data for _, n := range f.GetUI().Nodes { - if n.Group == node.DefaultGroup { - // we don't need the user to change the values here - // for better UX let's make them disabled - // when there are errors we won't hide the fields - if len(n.Messages) == 0 { - if input, ok := n.Attributes.(*node.InputAttributes); ok { - input.Type = "hidden" - n.Attributes = input - } + if n.ID() == "identifier" || n.ID() == "address" { + if input, ok := n.Attributes.(*node.InputAttributes); ok { + input.Type = "hidden" + n.Attributes = input + input.Name = "identifier" } freshNodes = append(freshNodes, n) } @@ -342,6 +398,18 @@ func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error resendNode = node.NewInputField("resend", "code", node.CodeGroup, node.InputAttributeTypeSubmit). WithMetaLabel(text.NewInfoNodeResendOTP()) + + // Insert a back button if we have a two-step registration screen, so that the + // user can navigate back to the credential selection screen. + if s.deps.Config().SelfServiceFlowRegistrationTwoSteps(ctx) { + backNode = node.NewInputField( + "screen", + "credential-selection", + node.ProfileGroup, + node.InputAttributeTypeSubmit, + ).WithMetaLabel(text.NewInfoRegistrationBack()) + } + default: return errors.WithStack(herodot.ErrBadRequest.WithReason("received an unexpected flow type")) } @@ -359,12 +427,16 @@ func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error // code submit button freshNodes. Append(node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit). - WithMetaLabel(text.NewInfoNodeLabelSubmit())) + WithMetaLabel(text.NewInfoNodeLabelContinue())) if resendNode != nil { freshNodes.Append(resendNode) } + if backNode != nil { + freshNodes.Append(backNode) + } + f.GetUI().Nodes = freshNodes f.GetUI().Method = "POST" diff --git a/selfservice/strategy/code/strategy_login.go b/selfservice/strategy/code/strategy_login.go index ac658e238dcb..d568df8beec5 100644 --- a/selfservice/strategy/code/strategy_login.go +++ b/selfservice/strategy/code/strategy_login.go @@ -4,11 +4,19 @@ package code import ( + "cmp" "context" - "database/sql" + "encoding/json" "net/http" "strings" + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/kratos/driver/config" + + "github.com/ory/kratos/selfservice/strategy/idfirst" + "github.com/ory/kratos/text" + "github.com/ory/x/sqlcon" "github.com/pkg/errors" @@ -28,7 +36,10 @@ import ( "github.com/ory/x/decoderx" ) -var _ login.Strategy = new(Strategy) +var ( + _ login.FormHydrator = new(Strategy) + _ login.Strategy = new(Strategy) +) // Update Login flow using the code method // @@ -54,26 +65,31 @@ type updateLoginFlowWithCodeMethod struct { // required: false Identifier string `json:"identifier" form:"identifier"` + // Address is the address to send the code to, in case that there are multiple addresses. This field + // is only used in two-factor flows and is ineffective for passwordless flows. + Address string `json:"address" form:"address"` + // Resend is set when the user wants to resend the code // required: false Resend string `json:"resend" form:"resend"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) RegisterLoginRoutes(*x.RouterPublic) {} -func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context, amr session.AuthenticationMethods) session.AuthenticationMethod { - aal1Satisfied := lo.ContainsBy(amr, func(am session.AuthenticationMethod) bool { - return am.Method != identity.CredentialsTypeCodeAuth && am.AAL == identity.AuthenticatorAssuranceLevel1 - }) - if aal1Satisfied { - return session.AuthenticationMethod{ - Method: identity.CredentialsTypeCodeAuth, - AAL: identity.AuthenticatorAssuranceLevel2, - } +func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.AuthenticationMethod { + aal := identity.AuthenticatorAssuranceLevel1 + if s.deps.Config().SelfServiceCodeStrategy(ctx).MFAEnabled { + aal = identity.AuthenticatorAssuranceLevel2 } + return session.AuthenticationMethod{ - Method: identity.CredentialsTypeCodeAuth, - AAL: identity.AuthenticatorAssuranceLevel1, + Method: s.ID(), + AAL: aal, } } @@ -83,83 +99,111 @@ func (s *Strategy) HandleLoginError(r *http.Request, f *login.Flow, body *update } if f != nil { - email := "" + identifier := "" if body != nil { - email = body.Identifier + identifier = cmp.Or(body.Address, body.Identifier) } - ds, err := s.deps.Config().DefaultIdentityTraitsSchemaURL(r.Context()) - if err != nil { - return err - } - identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) - if err != nil { - return err - } f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) - f.UI.GetNodes().Upsert( - node.NewInputField("identifier", email, node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). - WithMetaLabel(identifierLabel), - ) + identifierNode := node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden) + + identifierNode.Attributes.SetValue(identifier) + f.UI.GetNodes().Upsert(identifierNode) } return err } -func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, lf *login.Flow) error { - return s.PopulateMethod(r, lf) -} - // findIdentityByIdentifier returns the identity and the code credential for the given identifier. // If the identity does not have a code credential, it will attempt to find // the identity through other credentials matching the identifier. // the fallback mechanism is used for migration purposes of old accounts that do not have a code credential. -func (s *Strategy) findIdentityByIdentifier(ctx context.Context, identifier string) (_ *identity.Identity, isFallback bool, err error) { +func (s *Strategy) findIdentityByIdentifier(ctx context.Context, identifier string) (id *identity.Identity, cred *identity.Credentials, isFallback bool, err error) { ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.strategy.findIdentityByIdentifier") defer otelx.End(span, &err) - id, cred, err := s.deps.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, s.ID(), identifier) + id, cred, err = s.deps.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, s.ID(), identifier) if errors.Is(err, sqlcon.ErrNoRows) { // this is a migration for old identities that do not have a code credential // we might be able to do a fallback login since we could not find a credential on this identifier // Case insensitive because we only care about emails. id, err := s.deps.PrivilegedIdentityPool().FindIdentityByCredentialIdentifier(ctx, identifier, false) if err != nil { - return nil, false, errors.WithStack(schema.NewNoCodeAuthnCredentials()) + return nil, nil, false, errors.WithStack(schema.NewNoCodeAuthnCredentials()) } // we don't know if the user has verified the code yet, so we just return the identity // and let the caller decide what to do with it - return id, true, nil + return id, nil, true, nil } else if err != nil { - return nil, false, errors.WithStack(schema.NewNoCodeAuthnCredentials()) + return nil, nil, false, errors.WithStack(schema.NewNoCodeAuthnCredentials()) } if len(cred.Identifiers) == 0 { - return nil, false, errors.WithStack(schema.NewNoCodeAuthnCredentials()) + return nil, nil, false, errors.WithStack(schema.NewNoCodeAuthnCredentials()) } // we don't need the code credential, we just need to know that it exists - return id, false, nil + return id, cred, false, nil } -func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, sess *session.Session) (_ *identity.Identity, err error) { - ctx, span := s.deps.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.code.strategy.Login") - defer otelx.End(span, &err) +type decodedMethod struct { + Method string `json:"method" form:"method"` + Address string `json:"address" form:"address"` +} - if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.deps); err != nil { - return nil, err +func (s *Strategy) methodEnabledAndAllowedFromRequest(r *http.Request, f *login.Flow) (*decodedMethod, error) { + var method decodedMethod + + compiler, err := decoderx.HTTPRawJSONSchemaCompiler(loginMethodSchema) + if err != nil { + return nil, errors.WithStack(err) } - var aal identity.AuthenticatorAssuranceLevel + if err := decoderx.NewHTTP().Decode(r, &method, compiler, + decoderx.HTTPKeepRequestBody(true), + decoderx.HTTPDecoderAllowedMethods("POST", "PUT", "PATCH", "GET"), + decoderx.HTTPDecoderSetValidatePayloads(false), + decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { + return &method, errors.WithStack(err) + } + + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.ID().String(), method.Method, s.deps); err != nil { + return &method, err + } + + return &method, nil +} + +func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, sess *session.Session) (_ *identity.Identity, err error) { + ctx, span := s.deps.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.code.Strategy.Login") + defer otelx.End(span, &err) if s.deps.Config().SelfServiceCodeStrategy(ctx).PasswordlessEnabled { - aal = identity.AuthenticatorAssuranceLevel1 + if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil { + return nil, err + } } else if s.deps.Config().SelfServiceCodeStrategy(ctx).MFAEnabled { - aal = identity.AuthenticatorAssuranceLevel2 + if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel2); err != nil { + return nil, err + } + } else { + return nil, errors.WithStack(flow.ErrStrategyNotResponsible) } - if err := login.CheckAAL(f, aal); err != nil { + if p, err := s.methodEnabledAndAllowedFromRequest(r, f); errors.Is(err, flow.ErrStrategyNotResponsible) { + if !s.deps.Config().SelfServiceCodeStrategy(ctx).MFAEnabled { + span.SetAttributes(attribute.String("not_responsible_reason", "MFA is not enabled")) + return nil, err + } + + if p == nil || len(p.Address) == 0 { + span.SetAttributes(attribute.String("not_responsible_reason", "method not set or address not set")) + return nil, err + } + + // In this special case we only expect `address` to be set. + } else if err != nil { return nil, err } @@ -173,6 +217,8 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, s.HandleLoginError(r, f, &p, err) } + f.TransientPayload = p.TransientPayload + if err := flow.EnsureCSRF(s.deps, r, f.Type, s.deps.Config().DisableAPIFlowEnforcement(ctx), s.deps.GenerateCSRFToken, p.CSRFToken); err != nil { return nil, s.HandleLoginError(r, f, &p, err) } @@ -187,7 +233,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, } return nil, nil case flow.StateEmailSent: - i, err := s.loginVerifyCode(ctx, r, f, &p) + i, err := s.loginVerifyCode(ctx, f, &p, sess) if err != nil { return nil, s.HandleLoginError(r, f, &p, err) } @@ -199,40 +245,156 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, s.HandleLoginError(r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unexpected flow state: %s", f.GetState()))) } -func (s *Strategy) loginSendCode(ctx context.Context, w http.ResponseWriter, r *http.Request, f *login.Flow, p *updateLoginFlowWithCodeMethod, sess *session.Session) (err error) { - ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.strategy.loginSendCode") +func (s *Strategy) findIdentifierInVerifiableAddress(i *identity.Identity, identifier string) (*Address, error) { + verifiableAddress, found := lo.Find(i.VerifiableAddresses, func(va identity.VerifiableAddress) bool { + return va.Value == identifier + }) + if !found { + return nil, errors.WithStack(schema.NewUnknownAddressError()) + } + + // This should be fine for legacy cases because we use `UpgradeCredentials` to normalize all address types prior + // to calling this method. + parsed, err := identity.NewCodeChannel(verifiableAddress.Via) + if err != nil { + return nil, err + } + + return &Address{ + To: verifiableAddress.Value, + Via: parsed, + }, nil +} + +func (s *Strategy) findIdentityForIdentifier(ctx context.Context, identifier string, requestedAAL identity.AuthenticatorAssuranceLevel, session *session.Session) (_ *identity.Identity, _ []Address, err error) { + ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.strategy.findIdentityForIdentifier") + span.SetAttributes( + attribute.String("flow.requested_aal", string(requestedAAL)), + ) + defer otelx.End(span, &err) - if len(p.Identifier) == 0 { - return errors.WithStack(schema.NewRequiredError("#/identifier", "identifier")) + if len(identifier) == 0 { + return nil, nil, errors.WithStack(schema.NewRequiredError("#/identifier", "identifier")) } - p.Identifier = maybeNormalizeEmail(p.Identifier) + identifier = x.GracefulNormalization(identifier) var addresses []Address - var i *identity.Identity - if f.RequestedAAL > identity.AuthenticatorAssuranceLevel1 { - address, found := lo.Find(sess.Identity.VerifiableAddresses, func(va identity.VerifiableAddress) bool { - return va.Value == p.Identifier - }) - if !found { - return errors.WithStack(schema.NewUnknownAddressError()) + + // Step 1: Get the identity + i, cred, isFallback, err := s.findIdentityByIdentifier(ctx, identifier) + if err != nil { + if requestedAAL == identity.AuthenticatorAssuranceLevel2 { + // When using two-factor auth, the identity used to not have any code credential associated. Therefore, + // we need to gracefully handle this flow. + // + // TODO this section should be removed at some point when we are sure that all identities have a code credential. + if codeCred := new(schema.ValidationError); errors.As(err, &codeCred) && codeCred.ValidationError.Message == "account does not exist or has not setup up sign in with code" { + fallbackAllowed := s.deps.Config().SelfServiceCodeMethodMissingCredentialFallbackEnabled(ctx) + span.SetAttributes( + attribute.Bool(config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, fallbackAllowed), + ) + + if !fallbackAllowed { + s.deps.Logger().Warn("The identity does not have a code credential but the fallback mechanism is disabled. Login failed.") + return nil, nil, errors.WithStack(schema.NewNoCodeAuthnCredentials()) + } + + address, err := s.findIdentifierInVerifiableAddress(session.Identity, identifier) + if err != nil { + return nil, nil, err + } + + // We only end up here if the identity's identity schema does not have the `code` identifier extension defined. + // We know that this is the case for a couple of projects who use 2FA with the code credential. + // + // In those scenarios, the identity has no code credential, and the code credential will also not be created by + // the identity schema. + // + // To avoid future regressions, we will not perform an update on the identity here. Effectively, whenever + // the identity would be updated again (and the identity schema + extensions parsed), it would be likely + // that the code credentials are overwritten. + // + // So we accept that the identity in this case will simply not have code credentials, and we will rely on the + // fallback mechanism to authenticate the user. + return session.Identity, []Address{*address}, nil + } else if err != nil { + return nil, nil, err + } } - i = sess.Identity + return nil, nil, err + } else if isFallback { + fallbackAllowed := s.deps.Config().SelfServiceCodeMethodMissingCredentialFallbackEnabled(ctx) + span.SetAttributes( + attribute.String("identity.id", i.ID.String()), + attribute.String("network.id", i.NID.String()), + attribute.Bool(config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, fallbackAllowed), + ) + + if !fallbackAllowed { + s.deps.Logger().Warn("The identity does not have a code credential but the fallback mechanism is disabled. Login failed.") + return nil, nil, errors.WithStack(schema.NewNoCodeAuthnCredentials()) + } + + // We don't have a code credential, but we can still login the user if they have a verified address. + // This is a migration path for old accounts that do not have a code credential. addresses = []Address{{ - To: address.Value, - Via: address.Via, + To: identifier, + Via: identity.CodeChannelEmail, }} + + // We only end up here if the identity's identity schema does not have the `code` identifier extension defined. + // We know that this is the case for a couple of projects who use 2FA with the code credential. + // + // In those scenarios, the identity has no code credential, and the code credential will also not be created by + // the identity schema. + // + // To avoid future regressions, we will not perform an update on the identity here. Effectively, whenever + // the identity would be updated again (and the identity schema + extensions parsed), it would be likely + // that the code credentials are overwritten. + // + // So we accept that the identity in this case will simply not have code credentials, and we will rely on the + // fallback mechanism to authenticate the user. } else { - // Step 1: Get the identity - i, _, err = s.findIdentityByIdentifier(ctx, p.Identifier) - if err != nil { - return err + span.SetAttributes( + attribute.String("identity.id", i.ID.String()), + attribute.String("network.id", i.NID.String()), + ) + + var conf identity.CredentialsCode + if err := json.Unmarshal(cred.Config, &conf); err != nil { + return nil, nil, errors.WithStack(err) } - addresses = []Address{{ - To: p.Identifier, - Via: identity.CodeAddressType(identity.AddressTypeEmail), - }} + + for _, address := range conf.Addresses { + addresses = append(addresses, Address{ + To: address.Address, + Via: address.Channel, + }) + } + } + + return i, addresses, nil +} + +func (s *Strategy) loginSendCode(ctx context.Context, w http.ResponseWriter, r *http.Request, f *login.Flow, p *updateLoginFlowWithCodeMethod, sess *session.Session) (err error) { + ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.strategy.loginSendCode") + defer otelx.End(span, &err) + + p.Identifier = maybeNormalizeEmail( + cmp.Or(p.Identifier, p.Address), + ) + + i, addresses, err := s.findIdentityForIdentifier(ctx, p.Identifier, f.RequestedAAL, sess) + if err != nil { + return err + } + + if address, found := lo.Find(addresses, func(item Address) bool { + return item.To == x.GracefulNormalization(p.Identifier) + }); found { + addresses = []Address{address} } // Step 2: Delete any previous login codes for this flow ID @@ -277,7 +439,7 @@ func maybeNormalizeEmail(input string) string { return input } -func (s *Strategy) loginVerifyCode(ctx context.Context, r *http.Request, f *login.Flow, p *updateLoginFlowWithCodeMethod) (_ *identity.Identity, err error) { +func (s *Strategy) loginVerifyCode(ctx context.Context, f *login.Flow, p *updateLoginFlowWithCodeMethod, sess *session.Session) (_ *identity.Identity, err error) { ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.strategy.loginVerifyCode") defer otelx.End(span, &err) @@ -287,27 +449,21 @@ func (s *Strategy) loginVerifyCode(ctx context.Context, r *http.Request, f *logi return nil, errors.WithStack(schema.NewRequiredError("#/code", "code")) } - if len(p.Identifier) == 0 { - return nil, errors.WithStack(schema.NewRequiredError("#/identifier", "identifier")) - } - - p.Identifier = maybeNormalizeEmail(p.Identifier) + p.Identifier = maybeNormalizeEmail( + cmp.Or( + p.Address, + p.Identifier, // Older versions of Kratos required us to send the identifier here. + ), + ) - isFallback := false var i *identity.Identity - if f.RequestedAAL > identity.AuthenticatorAssuranceLevel1 { - // Don't require the code credential if the user already has a session (e.g. this is an MFA flow) - sess, err := s.deps.SessionManager().FetchFromRequest(ctx, r) - if err != nil { - return nil, err - } + if f.RequestedAAL == identity.AuthenticatorAssuranceLevel2 { i = sess.Identity } else { - // Step 1: Get the identity - i, isFallback, err = s.findIdentityByIdentifier(ctx, p.Identifier) - if err != nil { - return nil, err - } + i, _, err = s.findIdentityForIdentifier(ctx, p.Identifier, f.RequestedAAL, sess) + } + if err != nil { + return nil, err } loginCode, err := s.deps.LoginCodePersister().UseLoginCode(ctx, f.ID, i.ID, p.Code) @@ -323,22 +479,6 @@ func (s *Strategy) loginVerifyCode(ctx context.Context, r *http.Request, f *logi return nil, errors.WithStack(err) } - // the code is correct, if the login happened through a different credential, we need to update the identity - if isFallback { - if err := i.SetCredentialsWithConfig( - s.ID(), - // p.Identifier was normalized prior. - identity.Credentials{Type: s.ID(), Identifiers: []string{p.Identifier}}, - &identity.CredentialsCode{UsedAt: sql.NullTime{}}, - ); err != nil { - return nil, errors.WithStack(err) - } - - if err := s.deps.PrivilegedIdentityPool().UpdateIdentity(ctx, i); err != nil { - return nil, errors.WithStack(err) - } - } - // Step 2: The code was correct f.Active = identity.CredentialsTypeCodeAuth @@ -350,6 +490,14 @@ func (s *Strategy) loginVerifyCode(ctx context.Context, r *http.Request, f *logi return nil, errors.WithStack(err) } + // Step 3: Verify the address + if err := s.verifyAddress(ctx, i, Address{ + To: loginCode.Address, + Via: loginCode.AddressType, + }); err != nil { + return nil, err + } + for idx := range i.VerifiableAddresses { va := i.VerifiableAddresses[idx] if !va.Verified && loginCode.Address == va.Value { @@ -364,3 +512,66 @@ func (s *Strategy) loginVerifyCode(ctx context.Context, r *http.Request, f *logi return i, nil } + +func (s *Strategy) verifyAddress(ctx context.Context, i *identity.Identity, verified Address) error { + for idx := range i.VerifiableAddresses { + va := i.VerifiableAddresses[idx] + if va.Verified { + continue + } + + if verified.To != va.Value || string(verified.Via) != va.Via { + continue + } + + va.Verified = true + va.Status = identity.VerifiableAddressStatusCompleted + if err := s.deps.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, &va); errors.Is(err, sqlcon.ErrNoRows) { + // This happens when the verified address does not yet exist, for example during registration. In this case we just skip. + continue + } else if err != nil { + return err + } + break + } + + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, f *login.Flow) error { + return s.PopulateMethod(r, f) +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, f *login.Flow) error { + return s.PopulateMethod(r, f) +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, f *login.Flow) error { + return s.PopulateMethod(r, f) +} + +func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, f *login.Flow) error { + return s.PopulateMethod(r, f) +} + +func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, f *login.Flow, opts ...login.FormHydratorModifier) error { + if !s.deps.Config().SelfServiceCodeStrategy(r.Context()).PasswordlessEnabled { + // We only return this if passwordless is disabled, because if it is enabled we can always sign in using this method. + return errors.WithStack(idfirst.ErrNoCredentialsFound) + } + o := login.NewFormHydratorOptions(opts) + + // If the identity hint is nil and account enumeration mitigation is disabled, we return an error. + if o.IdentityHint == nil && !s.deps.Config().SecurityAccountEnumerationMitigate(r.Context()) { + return errors.WithStack(idfirst.ErrNoCredentialsFound) + } + + f.GetUI().Nodes.Append( + node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceLoginCode()), + ) + return nil +} + +func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, f *login.Flow) error { + return nil +} diff --git a/selfservice/strategy/code/strategy_login_test.go b/selfservice/strategy/code/strategy_login_test.go index 19cac6d38375..d93972162852 100644 --- a/selfservice/strategy/code/strategy_login_test.go +++ b/selfservice/strategy/code/strategy_login_test.go @@ -10,7 +10,20 @@ import ( "net/http" "net/http/httptest" "net/url" + "strings" "testing" + "time" + + "github.com/ory/kratos/courier" + + "github.com/ory/kratos/selfservice/strategy/idfirst" + + configtesthelpers "github.com/ory/kratos/driver/config/testhelpers" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/selfservice/flow/login" + + "github.com/ory/kratos/selfservice/flow" "github.com/ory/x/ioutilx" "github.com/ory/x/snapshotx" @@ -32,6 +45,42 @@ import ( "github.com/ory/x/sqlxx" ) +func createIdentity(ctx context.Context, t *testing.T, reg driver.Registry, withoutCodeCredential bool, moreIdentifiers ...string) *identity.Identity { + t.Helper() + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.NID = x.NewUUID() + email := testhelpers.RandomEmail() + + ids := fmt.Sprintf(`"email":"%s"`, email) + for i, identifier := range moreIdentifiers { + ids = fmt.Sprintf(`%s,"email_%d":"%s"`, ids, i+1, identifier) + } + + i.Traits = identity.Traits(fmt.Sprintf(`{"tos": true, %s}`, ids)) + + credentials := map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Identifiers: append([]string{email}, moreIdentifiers...), Type: identity.CredentialsTypePassword, Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}")}, + identity.CredentialsTypeOIDC: {Type: identity.CredentialsTypeOIDC, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}")}, + identity.CredentialsTypeWebAuthn: {Type: identity.CredentialsTypeWebAuthn, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\", \"user_handle\": \"rVIFaWRcTTuQLkXFmQWpgA==\"}")}, + } + if !withoutCodeCredential { + credentials[identity.CredentialsTypeCodeAuth] = identity.Credentials{Type: identity.CredentialsTypeCodeAuth, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage(`{"addresses":[{"channel":"email","address":"` + email + `"}]}`)} + } + i.Credentials = credentials + + var va []identity.VerifiableAddress + for _, identifier := range moreIdentifiers { + va = append(va, identity.VerifiableAddress{Value: identifier, Verified: false, Status: identity.VerifiableAddressStatusCompleted}) + } + + va = append(va, identity.VerifiableAddress{Value: email, Verified: true, Status: identity.VerifiableAddressStatusCompleted}) + + i.VerifiableAddresses = va + + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + return i +} + func TestLoginCodeStrategy(t *testing.T) { ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) @@ -46,41 +95,6 @@ func TestLoginCodeStrategy(t *testing.T) { public, _, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) - createIdentity := func(ctx context.Context, t *testing.T, withoutCodeCredential bool, moreIdentifiers ...string) *identity.Identity { - t.Helper() - i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) - email := testhelpers.RandomEmail() - - ids := fmt.Sprintf(`"email":"%s"`, email) - for i, identifier := range moreIdentifiers { - ids = fmt.Sprintf(`%s,"email_%d":"%s"`, ids, i+1, identifier) - } - - i.Traits = identity.Traits(fmt.Sprintf(`{"tos": true, %s}`, ids)) - - credentials := map[identity.CredentialsType]identity.Credentials{ - identity.CredentialsTypePassword: {Identifiers: append([]string{email}, moreIdentifiers...), Type: identity.CredentialsTypePassword, Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}")}, - identity.CredentialsTypeOIDC: {Type: identity.CredentialsTypeOIDC, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}")}, - identity.CredentialsTypeWebAuthn: {Type: identity.CredentialsTypeWebAuthn, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\", \"user_handle\": \"rVIFaWRcTTuQLkXFmQWpgA==\"}")}, - } - if !withoutCodeCredential { - credentials[identity.CredentialsTypeCodeAuth] = identity.Credentials{Type: identity.CredentialsTypeCodeAuth, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"address_type\": \"email\", \"used_at\": \"2023-07-26T16:59:06+02:00\"}")} - } - i.Credentials = credentials - - var va []identity.VerifiableAddress - for _, identifier := range moreIdentifiers { - va = append(va, identity.VerifiableAddress{Value: identifier, Verified: false, Status: identity.VerifiableAddressStatusCompleted}) - } - - va = append(va, identity.VerifiableAddress{Value: email, Verified: true, Status: identity.VerifiableAddressStatusCompleted}) - - i.VerifiableAddresses = va - - require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i)) - return i - } - type state struct { flowID string identity *identity.Identity @@ -99,11 +113,9 @@ func TestLoginCodeStrategy(t *testing.T) { ApiTypeNative ApiType = "api" ) - createLoginFlow := func(ctx context.Context, t *testing.T, public *httptest.Server, apiType ApiType, withoutCodeCredential bool, moreIdentifiers ...string) *state { + createLoginFlowWithIdentity := func(ctx context.Context, t *testing.T, public *httptest.Server, apiType ApiType, user *identity.Identity) *state { t.Helper() - identity := createIdentity(ctx, t, withoutCodeCredential, moreIdentifiers...) - var client *http.Client if apiType == ApiTypeNative { client = &http.Client{} @@ -130,24 +142,29 @@ func TestLoginCodeStrategy(t *testing.T) { require.NotEmptyf(t, csrfToken, "could not find csrf_token in: %s", body) } - loginEmail := gjson.Get(identity.Traits.String(), "email").String() - require.NotEmptyf(t, loginEmail, "could not find the email trait inside the identity: %s", identity.Traits.String()) - return &state{ - flowID: clientInit.GetId(), - identity: identity, - identityEmail: loginEmail, - client: client, - testServer: public, + flowID: clientInit.GetId(), + identity: user, + client: client, + testServer: public, } } + createLoginFlow := func(ctx context.Context, t *testing.T, public *httptest.Server, apiType ApiType, withoutCodeCredential bool, moreIdentifiers ...string) *state { + t.Helper() + s := createLoginFlowWithIdentity(ctx, t, public, apiType, createIdentity(ctx, t, reg, withoutCodeCredential, moreIdentifiers...)) + loginEmail := gjson.Get(s.identity.Traits.String(), "email").String() + require.NotEmptyf(t, loginEmail, "could not find the email trait inside the identity: %s", s.identity.Traits.String()) + s.identityEmail = loginEmail + return s + } + type onSubmitAssertion func(t *testing.T, s *state, body string, res *http.Response) submitLogin := func(ctx context.Context, t *testing.T, s *state, apiType ApiType, vals func(v *url.Values), mustHaveSession bool, submitAssertion onSubmitAssertion) *state { t.Helper() - lf, resp, err := testhelpers.NewSDKCustomClient(s.testServer, s.client).FrontendApi.GetLoginFlow(ctx).Id(s.flowID).Execute() + lf, resp, err := testhelpers.NewSDKCustomClient(s.testServer, s.client).FrontendAPI.GetLoginFlow(ctx).Id(s.flowID).Execute() require.NoError(t, err) require.EqualValues(t, http.StatusOK, resp.StatusCode) @@ -177,8 +194,8 @@ func TestLoginCodeStrategy(t *testing.T) { resp, err = s.client.Do(req) require.NoError(t, err) - require.EqualValues(t, http.StatusOK, resp.StatusCode) body = string(ioutilx.MustReadAll(resp.Body)) + require.EqualValues(t, http.StatusOK, resp.StatusCode, "%s", body) } else { // SPAs need to be informed that the login has not yet completed using status 400. // Browser clients will redirect back to the login URL. @@ -231,7 +248,7 @@ func TestLoginCodeStrategy(t *testing.T) { }, true, nil) }) - t.Run("case=should be able to log in with code", func(t *testing.T) { + t.Run("case=should be able to log in with code sent to email", func(t *testing.T) { // create login flow s := createLoginFlow(ctx, t, public, tc.apiType, false) @@ -247,9 +264,157 @@ func TestLoginCodeStrategy(t *testing.T) { assert.NotEmpty(t, loginCode) // 3. Submit OTP - submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + state := submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("code", loginCode) + }, true, nil) + if tc.apiType == ApiTypeSPA { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(state.body, "continue_with.0.action").String(), "%s", state.body) + assert.Contains(t, gjson.Get(state.body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", state.body) + } else { + assert.Empty(t, gjson.Get(state.body, "continue_with").Array(), "%s", state.body) + } + }) + + t.Run("case=should be able to log in legacy cases", func(t *testing.T) { + run := func(t *testing.T, s *state) { + // submit email + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("identifier", s.identityEmail) + }, false, nil) + + t.Logf("s.body: %s", s.body) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + // 3. Submit OTP + state := submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("code", loginCode) + }, true, nil) + if tc.apiType == ApiTypeSPA { + assert.Contains(t, gjson.Get(state.body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", state.body) + } else { + assert.Empty(t, gjson.Get(state.body, "continue_with").Array(), "%s", state.body) + } + } + + initDefault := func(t *testing.T, cf string) *state { + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.NID = x.NewUUID() + + // valid fake phone number for libphonenumber + email := testhelpers.RandomEmail() + i.Traits = identity.Traits(fmt.Sprintf(`{"tos": true, "email": "%s"}`, email)) + i.Credentials = map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeCodeAuth: { + Type: identity.CredentialsTypeCodeAuth, + Identifiers: []string{email}, + Version: 0, + Config: sqlxx.JSONRawMessage(cf), + }, + } + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentities(ctx, i)) // We explicitly bypass identity validation to test the legacy code path + s := createLoginFlowWithIdentity(ctx, t, public, tc.apiType, i) + s.identityEmail = email + return s + } + + t.Run("case=should be able to send address type with spaces", func(t *testing.T) { + run(t, + initDefault(t, `{"address_type": "email ", "used_at": {"Time": "0001-01-01T00:00:00Z", "Valid": false}}`), + ) + }) + + t.Run("case=should be able to send to empty address type", func(t *testing.T) { + run(t, + initDefault(t, `{"address_type": "", "used_at": {"Time": "0001-01-01T00:00:00Z", "Valid": false}}`), + ) + }) + + t.Run("case=should be able to send to empty credentials config", func(t *testing.T) { + run(t, + initDefault(t, `{}`), + ) + }) + + t.Run("case=should be able to send to identity with no credentials at all when fallback is enabled", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, true) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, nil) + }) + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.NID = x.NewUUID() + email := testhelpers.RandomEmail() + i.Traits = identity.Traits(fmt.Sprintf(`{"tos": true, "email": "%s"}`, email)) + i.Credentials = map[identity.CredentialsType]identity.Credentials{ + // This makes it possible for our code to find the identity identifier here. + identity.CredentialsTypePassword: {Type: identity.CredentialsTypePassword, Identifiers: []string{email}, Config: sqlxx.JSONRawMessage(`{}`)}, + } + + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentities(ctx, i)) // We explicitly bypass identity validation to test the legacy code path + s := createLoginFlowWithIdentity(ctx, t, public, tc.apiType, i) + s.identityEmail = email + run(t, s) + }) + + t.Run("case=should fail to send to identity with no credentials at all when fallback is disabled", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, false) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, nil) + }) + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.NID = x.NewUUID() + email := testhelpers.RandomEmail() + i.Traits = identity.Traits(fmt.Sprintf(`{"tos": true, "email": "%s"}`, email)) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentities(ctx, i)) // We explicitly bypass identity validation to test the legacy code path + s := createLoginFlowWithIdentity(ctx, t, public, tc.apiType, i) + s.identityEmail = email + // submit email + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("identifier", s.identityEmail) + }, false, nil) + assert.Contains(t, s.body, "4000035", "Should not find the account") + }) + }) + + t.Run("case=should be able to log in with code to sms and normalize the number", func(t *testing.T) { + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.NID = x.NewUUID() + + // valid fake phone number for libphonenumber + phone := "+1 (415) 55526-71" + i.Traits = identity.Traits(fmt.Sprintf(`{"tos": true, "phone_1": "%s"}`, phone)) + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + t.Cleanup(func() { + require.NoError(t, reg.PrivilegedIdentityPool().DeleteIdentity(ctx, i.ID)) + }) + + s := createLoginFlowWithIdentity(ctx, t, public, tc.apiType, i) + + // submit email + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("identifier", phone) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, x.GracefulNormalization(phone), "Your login code is:") + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + // 3. Submit OTP + state := submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { v.Set("code", loginCode) }, true, nil) + if tc.apiType == ApiTypeSPA { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(state.body, "continue_with.0.action").String(), "%s", state.body) + assert.Contains(t, gjson.Get(state.body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", state.body) + } else { + assert.Empty(t, gjson.Get(state.body, "continue_with").Array(), "%s", state.body) + } }) t.Run("case=new identities automatically have login with code", func(t *testing.T) { @@ -354,7 +519,7 @@ func TestLoginCodeStrategy(t *testing.T) { if tc.apiType == ApiTypeBrowser { require.EqualValues(t, http.StatusOK, resp.StatusCode) require.EqualValues(t, conf.SelfServiceFlowLoginUI(ctx).Path, resp.Request.URL.Path) - lf, resp, err := testhelpers.NewSDKCustomClient(public, s.client).FrontendApi.GetLoginFlow(ctx).Id(s.flowID).Execute() + lf, resp, err := testhelpers.NewSDKCustomClient(public, s.client).FrontendAPI.GetLoginFlow(ctx).Id(s.flowID).Execute() require.NoError(t, err) require.EqualValues(t, http.StatusOK, resp.StatusCode) body, err := json.Marshal(lf) @@ -378,7 +543,7 @@ func TestLoginCodeStrategy(t *testing.T) { require.EqualValues(t, http.StatusOK, resp.StatusCode) require.EqualValues(t, conf.SelfServiceFlowLoginUI(ctx).Path, resp.Request.URL.Path) - lf, resp, err := testhelpers.NewSDKCustomClient(public, s.client).FrontendApi.GetLoginFlow(ctx).Id(s.flowID).Execute() + lf, resp, err := testhelpers.NewSDKCustomClient(public, s.client).FrontendAPI.GetLoginFlow(ctx).Id(s.flowID).Execute() require.NoError(t, err) require.EqualValues(t, http.StatusOK, resp.StatusCode) body, err := json.Marshal(lf) @@ -464,7 +629,7 @@ func TestLoginCodeStrategy(t *testing.T) { // with browser clients we redirect back to the UI with a new flow id as a query parameter require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, conf.SelfServiceFlowLoginUI(ctx).Path, resp.Request.URL.Path) - lf, _, err := testhelpers.NewSDKCustomClient(public, s.client).FrontendApi.GetLoginFlow(ctx).Id(resp.Request.URL.Query().Get("flow")).Execute() + lf, _, err := testhelpers.NewSDKCustomClient(public, s.client).FrontendAPI.GetLoginFlow(ctx).Id(resp.Request.URL.Query().Get("flow")).Execute() require.NoError(t, err) require.EqualValues(t, http.StatusOK, resp.StatusCode) @@ -586,55 +751,284 @@ func TestLoginCodeStrategy(t *testing.T) { }) t.Run("case=should be able to get AAL2 session", func(t *testing.T) { - t.Cleanup(testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/default.schema.json")) // doesn't have the code credential - identity := createIdentity(ctx, t, true) + run := func(t *testing.T, withoutCodeCredential bool, overrideCodeCredential *identity.Credentials, overrideAllCredentials map[identity.CredentialsType]identity.Credentials) (*state, *http.Client) { + user := createIdentity(ctx, t, reg, withoutCodeCredential) + if overrideCodeCredential != nil { + toUpdate := user.Credentials[identity.CredentialsTypeCodeAuth] + if overrideCodeCredential.Config != nil { + toUpdate.Config = overrideCodeCredential.Config + } + if overrideCodeCredential.Identifiers != nil { + toUpdate.Identifiers = overrideCodeCredential.Identifiers + } + user.Credentials[identity.CredentialsTypeCodeAuth] = toUpdate + } + if overrideAllCredentials != nil { + user.Credentials = overrideAllCredentials + } + require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(ctx, user)) + + var cl *http.Client + var f *oryClient.LoginFlow + if tc.apiType == ApiTypeNative { + cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, user) + f = testhelpers.InitializeLoginFlowViaAPI(t, cl, public, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email")) + } else { + cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, ctx, reg, user) + f = testhelpers.InitializeLoginFlowViaBrowser(t, cl, public, false, tc.apiType == ApiTypeSPA, false, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email")) + } + + body, err := json.Marshal(f) + require.NoError(t, err) + require.Len(t, gjson.GetBytes(body, "ui.nodes.#(group==code)").Array(), 1, "%s", body) + require.Len(t, gjson.GetBytes(body, "ui.messages").Array(), 1, "%s", body) + require.EqualValues(t, gjson.GetBytes(body, "ui.messages.0.id").Int(), text.InfoSelfServiceLoginMFA, "%s", body) + + s := &state{ + flowID: f.GetId(), + identity: user, + client: cl, + testServer: public, + identityEmail: gjson.Get(user.Traits.String(), "email").String(), + } + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("identifier", s.identityEmail) + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + return submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("code", loginCode) + }, true, nil), cl + } + + t.Run("case=correct code credential without fallback works", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") // has code identifier + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, false) // fallback enabled + + _, cl := run(t, true, nil, nil) + testhelpers.EnsureAAL(t, cl, public, "aal2", "code") + }) + + t.Run("case=disabling mfa does not lock out the users", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") // has code identifier + + s, cl := run(t, true, nil, nil) + testhelpers.EnsureAAL(t, cl, public, "aal2", "code") + + email := gjson.GetBytes(s.identity.Traits, "email").String() + s.identityEmail = email + + // We change now disable code mfa and enable passwordless instead. + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.mfa_enabled", false) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", true) + + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", false) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.mfa_enabled", true) + }) + + s = createLoginFlowWithIdentity(ctx, t, public, tc.apiType, s.identity) + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("identifier", email) + v.Set("method", "code") + }, false, nil) + + message := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + loginResult := submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("code", loginCode) + }, true, nil) + + if tc.apiType == ApiTypeNative { + assert.EqualValues(t, "aal1", gjson.Get(loginResult.body, "session.authenticator_assurance_level").String()) + assert.EqualValues(t, "code", gjson.Get(loginResult.body, "session.authentication_methods.#(method==code).method").String()) + } else { + // The user should be able to sign in correctly even though, probably, the internal state was aal2 for available AAL. + res, err := s.client.Get(public.URL + session.RouteWhoami) + require.NoError(t, err) + assert.EqualValues(t, http.StatusOK, res.StatusCode, loginResult.body) + sess := x.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + + assert.EqualValues(t, "aal1", gjson.GetBytes(sess, "authenticator_assurance_level").String()) + assert.EqualValues(t, "code", gjson.GetBytes(sess, "authentication_methods.#(method==code).method").String()) + } + }) + + t.Run("case=missing code credential with fallback works when identity schema has the code identifier set", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") // has code identifier + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, true) // fallback enabled + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, false) + }) + + _, cl := run(t, false, nil, nil) + testhelpers.EnsureAAL(t, cl, public, "aal2", "code") + }) + + t.Run("case=missing code credential with fallback works even when identity schema has no code identifier set", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/no-code.schema.json") // missing the code identifier + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, true) // fallback enabled + t.Cleanup(func() { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, false) + }) + + _, cl := run(t, false, nil, nil) + testhelpers.EnsureAAL(t, cl, public, "aal2", "code") + }) + + t.Run("case=missing code credential with fallback works even when identity schema has no code identifier set", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/no-code-id.schema.json") // missing the code identifier + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, true) // fallback enabled + t.Cleanup(func() { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, false) + }) + + _, cl := run(t, true, &identity.Credentials{}, map[identity.CredentialsType]identity.Credentials{}) + testhelpers.EnsureAAL(t, cl, public, "aal2", "code") + }) + + t.Run("case=legacy code credential with fallback works when identity schema has the code identifier not set", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/no-code.schema.json") // has code identifier + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, true) // fallback enabled + t.Cleanup(func() { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") // has code identifier + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, false) + }) + + _, cl := run(t, false, &identity.Credentials{Config: []byte(`{"via":""}`)}, nil) + testhelpers.EnsureAAL(t, cl, public, "aal2", "code") + }) + + t.Run("case=legacy code credential with fallback works when identity schema has the code identifier not set", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/no-code.schema.json") // has code identifier + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, true) // fallback enabled + t.Cleanup(func() { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") // has code identifier + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, false) + }) + + for k, credentialsConfig := range []string{ + `{"address_type": "email ", "used_at": {"Time": "0001-01-01T00:00:00Z", "Valid": false}}`, + `{"address_type": "email", "used_at": {"Time": "0001-01-01T00:00:00Z", "Valid": false}}`, + `{"address_type": "", "used_at": {"Time": "0001-01-01T00:00:00Z", "Valid": false}}`, + `{"address_type": ""}`, + `{"address_type": "phone"}`, + `{}`, + } { + t.Run(fmt.Sprintf("config=%d", k), func(t *testing.T) { + _, cl := run(t, false, &identity.Credentials{Config: []byte(credentialsConfig)}, nil) + testhelpers.EnsureAAL(t, cl, public, "aal2", "code") + }) + } + }) + }) + + t.Run("case=without via parameter all options are shown", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code-mfa.identity.schema.json") + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, false) + t.Cleanup(func() { + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") + }) + var cl *http.Client var f *oryClient.LoginFlow + + user := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + user.NID = x.NewUUID() + email1 := "code-mfa-1" + string(tc.apiType) + "@ory.sh" + email2 := "code-mfa-2" + string(tc.apiType) + "@ory.sh" + phone1 := 4917613213110 if tc.apiType == ApiTypeNative { - cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, identity) - f = testhelpers.InitializeLoginFlowViaAPI(t, cl, public, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email")) - } else { - cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, reg, identity) - f = testhelpers.InitializeLoginFlowViaBrowser(t, cl, public, false, tc.apiType == ApiTypeSPA, false, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email")) + phone1 += 1 + } else if tc.apiType == ApiTypeSPA { + phone1 += 2 } + user.Traits = identity.Traits(fmt.Sprintf(`{"email1":"%s","email2":"%s","phone1":"+%d"}`, email1, email2, phone1)) + require.NoError(t, reg.IdentityManager().Create(ctx, user)) - body, err := json.Marshal(f) - require.NoError(t, err) - require.Len(t, gjson.GetBytes(body, "ui.nodes.#(group==code)").Array(), 1) - require.Len(t, gjson.GetBytes(body, "ui.messages").Array(), 1, "%s", body) - require.EqualValues(t, gjson.GetBytes(body, "ui.messages.0.id").Int(), text.InfoSelfServiceLoginMFA, "%s", body) + run := func(t *testing.T, identifierField string, identifier string) { + if tc.apiType == ApiTypeNative { + cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, user) + f = testhelpers.InitializeLoginFlowViaAPI(t, cl, public, false, testhelpers.InitFlowWithAAL("aal2")) + } else { + cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, ctx, reg, user) + f = testhelpers.InitializeLoginFlowViaBrowser(t, cl, public, false, tc.apiType == ApiTypeSPA, false, false, testhelpers.InitFlowWithAAL("aal2")) + } - s := &state{ - flowID: f.GetId(), - identity: identity, - client: cl, - testServer: public, - identityEmail: gjson.Get(identity.Traits.String(), "email").String(), + body, err := json.Marshal(f) + require.NoError(t, err) + + snapshotx.SnapshotT(t, json.RawMessage(gjson.GetBytes(body, "ui.nodes.#(group==code)#").Raw)) + require.Len(t, gjson.GetBytes(body, "ui.messages").Array(), 1, "%s", body) + require.EqualValues(t, gjson.GetBytes(body, "ui.messages.0.id").Int(), text.InfoSelfServiceLoginMFA, "%s", body) + + s := &state{ + flowID: f.GetId(), + identity: user, + client: cl, + testServer: public, + identityEmail: gjson.Get(user.Traits.String(), "email").String(), + } + + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Del("method") + v.Set(identifierField, identifier) + }, false, nil) + + var message *courier.Message + if !strings.HasPrefix(identifier, "+") { + // email + message = testhelpers.CourierExpectMessage(ctx, t, reg, x.GracefulNormalization(identifier), "Login to your account") + assert.Contains(t, message.Body, "please login to your account by entering the following code") + } else { + // SMS + message = testhelpers.CourierExpectMessage(ctx, t, reg, x.GracefulNormalization(identifier), "Your login code is:") + } + loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, loginCode) + + t.Logf("loginCode: %s", loginCode) + + s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { + v.Set("code", loginCode) + v.Set(identifierField, identifier) + }, true, nil) + + testhelpers.EnsureAAL(t, cl, public, "aal2", "code") } - s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { - v.Set("identifier", s.identityEmail) - }, true, nil) - message := testhelpers.CourierExpectMessage(ctx, t, reg, s.identityEmail, "Login to your account") - assert.Contains(t, message.Body, "please login to your account by entering the following code") - loginCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) - assert.NotEmpty(t, loginCode) + t.Run("field=identifier-email", func(t *testing.T) { + run(t, "identifier", email1) + }) - s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { - v.Set("code", loginCode) - }, true, nil) + t.Run("field=address-email", func(t *testing.T) { + run(t, "address", email2) + }) - testhelpers.EnsureAAL(t, cl, public, "aal2", "code") + t.Run("field=address-phone", func(t *testing.T) { + run(t, "address", fmt.Sprintf("+%d", phone1)) + }) }) + t.Run("case=cannot use different identifier", func(t *testing.T) { - identity := createIdentity(ctx, t, false) + identity := createIdentity(ctx, t, reg, false) var cl *http.Client var f *oryClient.LoginFlow if tc.apiType == ApiTypeNative { - cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, identity) + cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, identity) f = testhelpers.InitializeLoginFlowViaAPI(t, cl, public, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email")) } else { - cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, reg, identity) + cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, ctx, reg, identity) f = testhelpers.InitializeLoginFlowViaBrowser(t, cl, public, false, tc.apiType == ApiTypeSPA, false, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email")) } @@ -654,21 +1048,21 @@ func TestLoginCodeStrategy(t *testing.T) { email := testhelpers.RandomEmail() s = submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) { v.Set("identifier", email) - }, true, nil) + }, false, nil) - require.Equal(t, "The address you entered does not match any known addresses in the current account.", gjson.Get(s.body, "ui.messages.0.text").String(), "%s", body) + require.Equal(t, "This account does not exist or has not setup sign in with code.", gjson.Get(s.body, "ui.messages.0.text").String(), "%s", body) }) t.Run("case=verify initial payload", func(t *testing.T) { fixedEmail := fmt.Sprintf("fixed_mfa_test_%s@ory.sh", tc.apiType) - identity := createIdentity(ctx, t, false, fixedEmail) + identity := createIdentity(ctx, t, reg, false, fixedEmail) var cl *http.Client var f *oryClient.LoginFlow if tc.apiType == ApiTypeNative { - cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, identity) + cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, identity) f = testhelpers.InitializeLoginFlowViaAPI(t, cl, public, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email_1")) } else { - cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, reg, identity) + cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, ctx, reg, identity) f = testhelpers.InitializeLoginFlowViaBrowser(t, cl, public, false, tc.apiType == ApiTypeSPA, false, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email_1")) } @@ -678,15 +1072,15 @@ func TestLoginCodeStrategy(t *testing.T) { }) t.Run("case=using a non existing identity trait results in an error", func(t *testing.T) { - identity := createIdentity(ctx, t, false) + identity := createIdentity(ctx, t, reg, false) var cl *http.Client var res *http.Response var err error if tc.apiType == ApiTypeNative { - cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, identity) + cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, identity) res, err = cl.Get(public.URL + "/self-service/login/api?aal=aal2&via=doesnt_exist") } else { - cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, reg, identity) + cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, ctx, reg, identity) res, err = cl.Get(public.URL + "/self-service/login/browser?aal=aal2&via=doesnt_exist") } require.NoError(t, err) @@ -695,39 +1089,19 @@ func TestLoginCodeStrategy(t *testing.T) { if tc.apiType == ApiTypeNative { body = []byte(gjson.GetBytes(body, "error").Raw) } - require.Equal(t, "Trait does not exist in identity schema", gjson.GetBytes(body, "reason").String(), "%s", body) + require.Equal(t, "No value found for trait doesnt_exist in the current identity.", gjson.GetBytes(body, "reason").String(), "%s", body) }) - t.Run("case=missing via parameter results results in an error", func(t *testing.T) { - identity := createIdentity(ctx, t, false) - var cl *http.Client - var res *http.Response - var err error - if tc.apiType == ApiTypeNative { - cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, identity) - res, err = cl.Get(public.URL + "/self-service/login/api?aal=aal2") - } else { - cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, reg, identity) - res, err = cl.Get(public.URL + "/self-service/login/browser?aal=aal2") - } - require.NoError(t, err) - - body := ioutilx.MustReadAll(res.Body) - if tc.apiType == ApiTypeNative { - body = []byte(gjson.GetBytes(body, "error").Raw) - } - require.Equal(t, "AAL2 login via code requires the `via` query parameter", gjson.GetBytes(body, "reason").String(), "%s", body) - }) t.Run("case=unset trait in identity should lead to an error", func(t *testing.T) { - identity := createIdentity(ctx, t, false) + identity := createIdentity(ctx, t, reg, false) var cl *http.Client var res *http.Response var err error if tc.apiType == ApiTypeNative { - cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, identity) + cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, identity) res, err = cl.Get(public.URL + "/self-service/login/api?aal=aal2&via=email_1") } else { - cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, reg, identity) + cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, ctx, reg, identity) res, err = cl.Get(public.URL + "/self-service/login/browser?aal=aal2&via=email_1") } require.NoError(t, err) @@ -736,9 +1110,264 @@ func TestLoginCodeStrategy(t *testing.T) { if tc.apiType == ApiTypeNative { body = []byte(gjson.GetBytes(body, "error").Raw) } - require.Equal(t, "No value found for trait email_1 in the current identity", gjson.GetBytes(body, "reason").String(), "%s", body) + require.Equal(t, "No value found for trait email_1 in the current identity.", gjson.GetBytes(body, "reason").String(), "%s", body) }) }) }) } } + +func TestFormHydration(t *testing.T) { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), map[string]interface{}{ + "enabled": true, + "passwordless_enabled": true, + }) + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/code.identity.schema.json") + + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeCodeAuth) + require.NoError(t, err) + fh, ok := s.(login.FormHydrator) + require.True(t, ok) + + toSnapshot := func(t *testing.T, f *login.Flow) { + t.Helper() + // The CSRF token has a unique value that messes with the snapshot - ignore it. + f.UI.Nodes.ResetNodes("csrf_token") + snapshotx.SnapshotT(t, f.UI.Nodes) + } + newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) { + r := httptest.NewRequest("GET", "/self-service/login/browser", nil) + r = r.WithContext(ctx) + t.Helper() + f, err := login.NewFlow(conf, time.Minute, "csrf_token", r, flow.TypeBrowser) + require.NoError(t, err) + return r, f + } + + passwordlessEnabled := configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), map[string]interface{}{ + "enabled": true, + "passwordless_enabled": true, + "mfa_enabled": false, + }) + + mfaEnabled := configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), map[string]interface{}{ + "enabled": true, + "passwordless_enabled": false, + "mfa_enabled": true, + }) + + toMFARequest := func(t *testing.T, r *http.Request, f *login.Flow, traits string) { + f.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + r.URL = &url.URL{Path: "/", RawQuery: "via=email"} + // I only fear god. + r.Header = testhelpers.NewHTTPClientWithArbitrarySessionTokenAndTraits(t, ctx, reg, []byte(traits)).Transport.(*testhelpers.TransportWithHeader).GetHeader() + } + + t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) { + t.Run("case=code is used for 2fa but request is 1fa", func(t *testing.T) { + r, f := newFlow(mfaEnabled, t) + f.RequestedAAL = identity.AuthenticatorAssuranceLevel1 + require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("case=code is used for passwordless login and request is 1fa", func(t *testing.T) { + r, f := newFlow(passwordlessEnabled, t) + f.RequestedAAL = identity.AuthenticatorAssuranceLevel1 + require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f)) + toSnapshot(t, f) + }) + }) + + t.Run("method=PopulateLoginMethodFirstFactorRefresh", func(t *testing.T) { + t.Run("case=code is used for passwordless login and request is 1fa with refresh", func(t *testing.T) { + r, f := newFlow(passwordlessEnabled, t) + f.RequestedAAL = identity.AuthenticatorAssuranceLevel1 + f.Refresh = true + require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f)) + toSnapshot(t, f) + }) + + t.Run("case=code is used for 2fa and request is 1fa with refresh", func(t *testing.T) { + r, f := newFlow(mfaEnabled, t) + f.RequestedAAL = identity.AuthenticatorAssuranceLevel1 + f.Refresh = true + require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f)) + toSnapshot(t, f) + }) + }) + + t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) { + t.Run("using via", func(t *testing.T) { + test := func(t *testing.T, ctx context.Context, email string) { + r, f := newFlow(ctx, t) + toMFARequest(t, r, f, `{"email":"`+email+`"}`) + + // We still use the legacy hydrator under the hood here and thus need to set this correctly. + f.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + r.URL = &url.URL{Path: "/", RawQuery: "via=email"} + + require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f)) + toSnapshot(t, f) + } + + t.Run("case=code is used for 2fa", func(t *testing.T) { + test(t, mfaEnabled, "PopulateLoginMethodSecondFactor-code-mfa-via-2fa@ory.sh") + }) + + t.Run("case=code is used for passwordless login", func(t *testing.T) { + test(t, passwordlessEnabled, "PopulateLoginMethodSecondFactor-code-mfa-via-passwordless@ory.sh") + }) + }) + + t.Run("without via", func(t *testing.T) { + test := func(t *testing.T, ctx context.Context, traits string) { + r, f := newFlow(ctx, t) + toMFARequest(t, r, f, traits) + + // We still use the legacy hydrator under the hood here and thus need to set this correctly. + f.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + r.URL = &url.URL{Path: "/"} + + require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f)) + toSnapshot(t, f) + } + + t.Run("case=code is used for 2fa", func(t *testing.T) { + ctx = testhelpers.WithDefaultIdentitySchema(mfaEnabled, "file://./stub/code-mfa.identity.schema.json") + test(t, ctx, `{"email1":"PopulateLoginMethodSecondFactor-no-via-2fa-0@ory.sh","email2":"PopulateLoginMethodSecondFactor-no-via-2fa-1@ory.sh","phone1":"+4917655138291"}`) + }) + + t.Run("case=code is used for passwordless login", func(t *testing.T) { + ctx = testhelpers.WithDefaultIdentitySchema(passwordlessEnabled, "file://./stub/code-mfa.identity.schema.json") + test(t, ctx, `{"email1":"PopulateLoginMethodSecondFactor-no-via-passwordless-0@ory.sh","email2":"PopulateLoginMethodSecondFactor-no-via-passwordless-1@ory.sh","phone1":"+4917655138292"}`) + }) + }) + + t.Run("case=code is used for 2fa and request is 2fa", func(t *testing.T) { + r, f := newFlow(mfaEnabled, t) + toMFARequest(t, r, f, `{"email":"foo@ory.sh"}`) + require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("case=code is used for passwordless login and request is 2fa", func(t *testing.T) { + r, f := newFlow(passwordlessEnabled, t) + toMFARequest(t, r, f, `{"email":"foo@ory.sh"}`) + require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f)) + toSnapshot(t, f) + }) + }) + + t.Run("method=PopulateLoginMethodSecondFactorRefresh", func(t *testing.T) { + t.Run("case=code is used for 2fa and request is 2fa with refresh", func(t *testing.T) { + r, f := newFlow(mfaEnabled, t) + toMFARequest(t, r, f, `{"email":"foo@ory.sh"}`) + f.Refresh = true + require.NoError(t, fh.PopulateLoginMethodSecondFactorRefresh(r, f)) + toSnapshot(t, f) + }) + + t.Run("case=code is used for passwordless login and request is 2fa with refresh", func(t *testing.T) { + r, f := newFlow(passwordlessEnabled, t) + toMFARequest(t, r, f, `{"email":"foo@ory.sh"}`) + f.Refresh = true + require.NoError(t, fh.PopulateLoginMethodSecondFactorRefresh(r, f)) + toSnapshot(t, f) + }) + }) + + t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) { + t.Run("case=no options", func(t *testing.T) { + t.Run("case=code is used for 2fa", func(t *testing.T) { + r, f := newFlow(mfaEnabled, t) + require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("case=code is used for passwordless login", func(t *testing.T) { + r, f := newFlow(passwordlessEnabled, t) + require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f)) + toSnapshot(t, f) + }) + }) + + t.Run("case=WithIdentityHint", func(t *testing.T) { + t.Run("case=account enumeration mitigation enabled", func(t *testing.T) { + t.Run("case=code is used for 2fa", func(t *testing.T) { + r, f := newFlow( + configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true), + t, + ) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=code is used for passwordless login", func(t *testing.T) { + r, f := newFlow( + configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true), + t, + ) + require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com"))) + toSnapshot(t, f) + }) + }) + + t.Run("case=account enumeration mitigation disabled", func(t *testing.T) { + t.Run("case=with no identity", func(t *testing.T) { + t.Run("case=code is used for 2fa", func(t *testing.T) { + r, f := newFlow(mfaEnabled, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=code is used for passwordless login", func(t *testing.T) { + r, f := newFlow(passwordlessEnabled, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + }) + t.Run("case=identity has code method", func(t *testing.T) { + identifier := x.NewUUID().String() + id := createIdentity(ctx, t, reg, false, identifier) + + t.Run("case=code is used for 2fa", func(t *testing.T) { + r, f := newFlow(mfaEnabled, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=code is used for passwordless login", func(t *testing.T) { + r, f := newFlow(passwordlessEnabled, t) + require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id))) + toSnapshot(t, f) + }) + }) + + t.Run("case=identity does not have a code method", func(t *testing.T) { + id := identity.NewIdentity("default") + + t.Run("case=code is used for 2fa", func(t *testing.T) { + r, f := newFlow(mfaEnabled, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=code is used for passwordless login", func(t *testing.T) { + r, f := newFlow(passwordlessEnabled, t) + require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id))) + toSnapshot(t, f) + }) + }) + }) + }) + }) + + t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f)) + toSnapshot(t, f) + }) +} diff --git a/selfservice/strategy/code/strategy_mfa.go b/selfservice/strategy/code/strategy_mfa.go new file mode 100644 index 000000000000..89fe79c393e8 --- /dev/null +++ b/selfservice/strategy/code/strategy_mfa.go @@ -0,0 +1,57 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "encoding/json" + + "github.com/pkg/errors" + "github.com/samber/lo" + + "github.com/ory/herodot" + "github.com/ory/kratos/identity" +) + +func FindAllIdentifiers(i *identity.Identity) (result []Address) { + for _, a := range i.VerifiableAddresses { + if len(a.Via) == 0 || len(a.Value) == 0 { + continue + } + + result = append(result, Address{Via: identity.CodeChannel(a.Via), To: a.Value}) + } + return result +} + +func FindCodeAddressCandidates(i *identity.Identity, fallbackEnabled bool) (result []Address, found bool, _ error) { + // If no hint was given, we show all OTP addresses from the credentials. + creds, ok := i.GetCredentials(identity.CredentialsTypeCodeAuth) + if !ok { + if !fallbackEnabled { + // Without a fallback and with no credentials found, we can't really do a lot and exit early. + return nil, false, nil + } + + return FindAllIdentifiers(i), true, nil + } else { + var conf identity.CredentialsCode + if len(creds.Config) > 0 { + if err := json.Unmarshal(creds.Config, &conf); err != nil { + return nil, false, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to unmarshal credentials config: %s", err)) + } + } + + if len(conf.Addresses) == 0 { + if !fallbackEnabled { + // Without a fallback and with no credentials found, we can't really do a lot and exit early. + return nil, false, nil + } + + return FindAllIdentifiers(i), true, nil + } + return lo.Map(conf.Addresses, func(item identity.CredentialsCodeAddress, _ int) Address { + return Address{Via: item.Channel, To: item.Address} + }), true, nil + } +} diff --git a/selfservice/strategy/code/strategy_mfa_test.go b/selfservice/strategy/code/strategy_mfa_test.go new file mode 100644 index 000000000000..5a97b881b179 --- /dev/null +++ b/selfservice/strategy/code/strategy_mfa_test.go @@ -0,0 +1,161 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package code + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ory/kratos/identity" +) + +func TestFindAllIdentifiers(t *testing.T) { + tests := []struct { + name string + input *identity.Identity + expected []Address + }{ + { + name: "valid verifiable addresses", + input: &identity.Identity{ + VerifiableAddresses: []identity.VerifiableAddress{ + {Via: "email", Value: "user@example.com"}, + {Via: "sms", Value: "+1234567890"}, + }, + }, + expected: []Address{ + {Via: identity.CodeChannel("email"), To: "user@example.com"}, + {Via: identity.CodeChannel("sms"), To: "+1234567890"}, + }, + }, + { + name: "empty verifiable addresses", + input: &identity.Identity{ + VerifiableAddresses: []identity.VerifiableAddress{}, + }, + }, + { + name: "verifiable address with empty fields", + input: &identity.Identity{ + VerifiableAddresses: []identity.VerifiableAddress{ + {Via: "", Value: ""}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FindAllIdentifiers(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestFindCodeAddressCandidates(t *testing.T) { + tests := []struct { + name string + input *identity.Identity + fallbackEnabled bool + expected []Address + found bool + wantErr bool + }{ + { + name: "valid credentials with addresses", + input: &identity.Identity{ + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeCodeAuth: { + Config: []byte(`{"addresses":[{"channel":"email","address":"user@example.com"},{"channel":"sms","address":"+1234567890"}]}`), + }, + }, + }, + fallbackEnabled: false, + expected: []Address{ + {Via: identity.CodeChannel("email"), To: "user@example.com"}, + {Via: identity.CodeChannel("sms"), To: "+1234567890"}, + }, + found: true, + wantErr: false, + }, + { + name: "no credentials, fallback enabled", + input: &identity.Identity{ + VerifiableAddresses: []identity.VerifiableAddress{ + {Via: "email", Value: "user@example.com"}, + {Via: "sms", Value: "+1234567890"}, + }, + }, + fallbackEnabled: true, + expected: []Address{ + {Via: identity.CodeChannel("email"), To: "user@example.com"}, + {Via: identity.CodeChannel("sms"), To: "+1234567890"}, + }, + found: true, + wantErr: false, + }, + { + name: "no credentials, fallback disabled", + input: &identity.Identity{ + VerifiableAddresses: []identity.VerifiableAddress{ + {Via: "email", Value: "user@example.com"}, + {Via: "sms", Value: "+1234567890"}, + }, + }, + fallbackEnabled: false, + expected: nil, + found: false, + wantErr: false, + }, + { + name: "invalid credentials config", + input: &identity.Identity{ + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeCodeAuth: { + Config: []byte(`invalid`), + }, + }, + }, + fallbackEnabled: false, + expected: nil, + found: false, + wantErr: true, + }, + { + name: "invalid credentials config, fallback enabled, verifiable addresses exist", + input: &identity.Identity{ + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypeCodeAuth: { + Config: []byte(`invalid`), + }, + }, + VerifiableAddresses: []identity.VerifiableAddress{ + {Via: "email", Value: "user@example.com"}, + {Via: "sms", Value: "+1234567890"}, + }, + }, + fallbackEnabled: true, + expected: []Address{ + {Via: identity.CodeChannel("email"), To: "user@example.com"}, + {Via: identity.CodeChannel("sms"), To: "+1234567890"}, + }, + found: true, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, found, err := FindCodeAddressCandidates(tt.input, tt.fallbackEnabled) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + assert.Equal(t, tt.found, found) + } + }) + } +} diff --git a/selfservice/strategy/code/strategy_recovery.go b/selfservice/strategy/code/strategy_recovery.go index 6a64f82e876a..3d050b7b60f8 100644 --- a/selfservice/strategy/code/strategy_recovery.go +++ b/selfservice/strategy/code/strategy_recovery.go @@ -4,6 +4,7 @@ package code import ( + "encoding/json" "net/http" "net/url" "time" @@ -42,7 +43,7 @@ func (s *Strategy) PopulateRecoveryMethod(r *http.Request, f *recovery.Flow) err f.UI. GetNodes(). Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit). - WithMetaLabel(text.NewInfoNodeLabelSubmit())) + WithMetaLabel(text.NewInfoNodeLabelContinue())) return nil } @@ -83,9 +84,14 @@ type updateRecoveryFlowWithCodeMethod struct { // // required: true Method recovery.RecoveryMethod `json:"method"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } -func (s Strategy) isCodeFlow(f *recovery.Flow) bool { +func (s *Strategy) isCodeFlow(f *recovery.Flow) bool { value, err := f.Active.Value() if err != nil { return false @@ -94,11 +100,12 @@ func (s Strategy) isCodeFlow(f *recovery.Flow) bool { } func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.Flow) (err error) { - ctx, span := s.deps.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.code.strategy.Recover") + ctx, span := s.deps.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.code.Strategy.Recover") span.SetAttributes(attribute.String("selfservice_flows_recovery_use", s.deps.Config().SelfServiceFlowRecoveryUse(ctx))) defer otelx.End(span, &err) if !s.isCodeFlow(f) { + span.SetAttributes(attribute.String("not_responsible_reason", "not code flow")) return errors.WithStack(flow.ErrStrategyNotResponsible) } @@ -107,6 +114,8 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F return s.HandleRecoveryError(w, r, nil, body, err) } + f.TransientPayload = body.TransientPayload + if f.DangerousSkipCSRFCheck { s.deps.Logger(). WithRequest(r). @@ -153,9 +162,8 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F } switch recoveryFlow.State { - case flow.StateChooseMethod: - fallthrough - case flow.StateEmailSent: + case flow.StateChooseMethod, + flow.StateEmailSent: return s.recoveryHandleFormSubmission(w, r, recoveryFlow, body) case flow.StatePassedChallenge: // was already handled, do not allow retry @@ -183,9 +191,9 @@ func (s *Strategy) recoveryIssueSession(w http.ResponseWriter, r *http.Request, return s.retryRecoveryFlow(w, r, f.Type, RetryWithError(err)) } - sess, err := session.NewActiveSession(r, id, s.deps.Config(), time.Now().UTC(), - identity.CredentialsTypeRecoveryCode, identity.AuthenticatorAssuranceLevel1) - if err != nil { + sess := session.NewInactiveSession() + sess.CompletedLoginFor(identity.CredentialsTypeRecoveryCode, identity.AuthenticatorAssuranceLevel1) + if err := s.deps.SessionManager().ActivateSession(r, sess, id, time.Now().UTC()); err != nil { return s.retryRecoveryFlow(w, r, f.Type, RetryWithError(err)) } @@ -228,14 +236,13 @@ func (s *Strategy) recoveryIssueSession(w http.ResponseWriter, r *http.Request, } if s.deps.Config().UseContinueWithTransitions(ctx) { + redirectTo := sf.AppendTo(s.deps.Config().SelfServiceFlowSettingsUI(r.Context())).String() switch { - case f.Type.IsAPI(): - fallthrough - case x.IsJSONRequest(r): - f.ContinueWith = append(f.ContinueWith, flow.NewContinueWithSettingsUI(sf)) + case f.Type.IsAPI(), x.IsJSONRequest(r): + f.ContinueWith = append(f.ContinueWith, flow.NewContinueWithSettingsUI(sf, redirectTo)) s.deps.Writer().Write(w, r, f) default: - http.Redirect(w, r, sf.AppendTo(s.deps.Config().SelfServiceFlowSettingsUI(r.Context())).String(), http.StatusSeeOther) + http.Redirect(w, r, redirectTo, http.StatusSeeOther) } } else { if x.IsJSONRequest(r) { @@ -378,6 +385,7 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R return s.HandleRecoveryError(w, r, f, body, err) } + f.TransientPayload = body.TransientPayload if err := s.deps.CodeSender().SendRecoveryCode(ctx, f, identity.VerifiableAddressTypeEmail, body.Email); err != nil { if !errors.Is(err, ErrUnknownAddress) { return s.HandleRecoveryError(w, r, f, body, err) @@ -399,6 +407,7 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R f.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithInputAttributes(func(a *node.InputAttributes) { a.Required = true a.Pattern = "[0-9]+" + a.MaxLength = CodeLength })). WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()), ) @@ -407,7 +416,7 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R f.UI. GetNodes(). Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit). - WithMetaLabel(text.NewInfoNodeLabelSubmit())) + WithMetaLabel(text.NewInfoNodeLabelContinue())) f.UI.Nodes.Append(node.NewInputField("email", body.Email, node.CodeGroup, node.InputAttributeTypeSubmit). WithMetaLabel(text.NewInfoNodeResendOTP()), @@ -460,11 +469,12 @@ func (s *Strategy) HandleRecoveryError(w http.ResponseWriter, r *http.Request, f } type recoverySubmitPayload struct { - Method string `json:"method" form:"method"` - Code string `json:"code" form:"code"` - CSRFToken string `json:"csrf_token" form:"csrf_token"` - Flow string `json:"flow" form:"flow"` - Email string `json:"email" form:"email"` + Method string `json:"method" form:"method"` + Code string `json:"code" form:"code"` + CSRFToken string `json:"csrf_token" form:"csrf_token"` + Flow string `json:"flow" form:"flow"` + Email string `json:"email" form:"email"` + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) decodeRecovery(r *http.Request) (*recoverySubmitPayload, error) { diff --git a/selfservice/strategy/code/strategy_recovery_admin.go b/selfservice/strategy/code/strategy_recovery_admin.go index 8964682afe30..63aa36a90edd 100644 --- a/selfservice/strategy/code/strategy_recovery_admin.go +++ b/selfservice/strategy/code/strategy_recovery_admin.go @@ -73,6 +73,13 @@ type createRecoveryCodeForIdentityBody struct { // - 1m // - 1s ExpiresIn string `json:"expires_in"` + + // Flow Type + // + // The flow type for the recovery flow. Defaults to browser. + // + // required: false + FlowType *flow.Type `json:"flow_type"` } // Recovery Code for Identity @@ -149,12 +156,21 @@ func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http. } } - if time.Now().Add(expiresIn).Before(time.Now()) { - s.deps.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Value from "expires_in" must result to a future time: %s`, p.ExpiresIn))) + if expiresIn <= 0 { + s.deps.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Value from "expires_in" must result to a future time: %s`, expiresIn))) + return + } + + flowType := flow.TypeBrowser + if p.FlowType != nil { + flowType = *p.FlowType + } + if !flowType.Valid() { + s.deps.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Value from "flow_type" is not valid: %q`, flowType))) return } - recoveryFlow, err := recovery.NewFlow(config, expiresIn, s.deps.GenerateCSRFToken(r), r, s, flow.TypeBrowser) + recoveryFlow, err := recovery.NewFlow(config, expiresIn, s.deps.GenerateCSRFToken(r), r, s, flowType) if err != nil { s.deps.Writer().WriteError(w, r, err) return @@ -162,13 +178,16 @@ func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http. recoveryFlow.DangerousSkipCSRFCheck = true recoveryFlow.State = flow.StateEmailSent recoveryFlow.UI.Nodes = node.Nodes{} - recoveryFlow.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). + recoveryFlow.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute, node.WithInputAttributes(func(a *node.InputAttributes) { + a.Pattern = "[0-9]+" + a.MaxLength = CodeLength + })). WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()), ) recoveryFlow.UI.Nodes. Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit). - WithMetaLabel(text.NewInfoNodeLabelSubmit())) + WithMetaLabel(text.NewInfoNodeLabelContinue())) if err := s.deps.RecoveryFlowPersister().CreateRecoveryFlow(ctx, recoveryFlow); err != nil { s.deps.Writer().WriteError(w, r, err) diff --git a/selfservice/strategy/code/strategy_recovery_admin_test.go b/selfservice/strategy/code/strategy_recovery_admin_test.go index aed7bbcbaf43..9a940ff8e31b 100644 --- a/selfservice/strategy/code/strategy_recovery_admin_test.go +++ b/selfservice/strategy/code/strategy_recovery_admin_test.go @@ -8,8 +8,10 @@ import ( "encoding/json" "fmt" "net/http" + "net/http/cookiejar" "net/http/httptest" "net/url" + "strings" "testing" "time" @@ -18,13 +20,14 @@ import ( "github.com/stretchr/testify/require" "github.com/tidwall/gjson" + "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" kratos "github.com/ory/kratos/internal/httpclient" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/recovery" - "github.com/ory/kratos/selfservice/strategy/code" + . "github.com/ory/kratos/selfservice/strategy/code" "github.com/ory/kratos/x" "github.com/ory/x/ioutilx" "github.com/ory/x/pointerx" @@ -35,6 +38,7 @@ func TestAdminStrategy(t *testing.T) { ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) initViper(t, ctx, conf) + conf.MustSet(ctx, config.ViperKeyUseContinueWithTransitions, true) _ = testhelpers.NewRecoveryUIFlowEchoServer(t, reg) _ = testhelpers.NewSettingsUIFlowEchoServer(t, reg) @@ -44,14 +48,11 @@ func TestAdminStrategy(t *testing.T) { publicTS, adminTS := testhelpers.NewKratosServer(t, reg) adminSDK := testhelpers.NewSDKClient(adminTS) - createCode := func(id string, expiresIn *string) (*kratos.RecoveryCodeForIdentity, *http.Response, error) { - return adminSDK.IdentityApi. + type createCodeParams = kratos.CreateRecoveryCodeForIdentityBody + createCode := func(params createCodeParams) (*kratos.RecoveryCodeForIdentity, *http.Response, error) { + return adminSDK.IdentityAPI. CreateRecoveryCodeForIdentity(context.Background()). - CreateRecoveryCodeForIdentityBody( - kratos.CreateRecoveryCodeForIdentityBody{ - IdentityId: id, - ExpiresIn: expiresIn, - }).Execute() + CreateRecoveryCodeForIdentityBody(params).Execute() } t.Run("no panic on empty body #1384", func(t *testing.T) { @@ -63,39 +64,42 @@ func TestAdminStrategy(t *testing.T) { f, err := recovery.NewFlow(reg.Config(), time.Minute, "", r, s, flow.TypeBrowser) require.NoError(t, err) require.NotPanics(t, func() { - require.Error(t, s.(*code.Strategy).HandleRecoveryError(w, r, f, nil, errors.New("test"))) + require.Error(t, s.(*Strategy).HandleRecoveryError(w, r, f, nil, errors.New("test"))) }) }) t.Run("description=should not be able to recover an account that does not exist", func(t *testing.T) { - _, _, err := createCode(x.NewUUID().String(), nil) + _, _, err := createCode(createCodeParams{IdentityId: x.NewUUID().String()}) require.IsType(t, err, new(kratos.GenericOpenAPIError), "%T", err) snapshotx.SnapshotT(t, err.(*kratos.GenericOpenAPIError).Model()) }) t.Run("description=should fail on malformed expiry time", func(t *testing.T) { - _, _, err := createCode(x.NewUUID().String(), pointerx.String("not-a-valid-value")) + _, _, err := createCode(createCodeParams{IdentityId: x.NewUUID().String(), ExpiresIn: pointerx.Ptr("not-a-valid-value")}) require.IsType(t, err, new(kratos.GenericOpenAPIError), "%T", err) snapshotx.SnapshotT(t, err.(*kratos.GenericOpenAPIError).Model()) }) t.Run("description=should fail on negative expiry time", func(t *testing.T) { - _, _, err := createCode(x.NewUUID().String(), pointerx.String("-1h")) + _, _, err := createCode(createCodeParams{IdentityId: x.NewUUID().String(), ExpiresIn: pointerx.Ptr("-1h")}) require.IsType(t, err, new(kratos.GenericOpenAPIError), "%T", err) snapshotx.SnapshotT(t, err.(*kratos.GenericOpenAPIError).Model()) }) - submitRecoveryLink := func(t *testing.T, link string, code string) []byte { + submitRecoveryCode := func(t *testing.T, client *http.Client, link string, code string) []byte { t.Helper() - res, err := publicTS.Client().Get(link) + if client == nil { + client = publicTS.Client() + } + res, err := client.Get(link) require.NoError(t, err) body := ioutilx.MustReadAll(res.Body) action := gjson.GetBytes(body, "ui.action").String() require.NotEmpty(t, action) - res, err = publicTS.Client().PostForm(action, url.Values{ + res, err = client.PostForm(action, url.Values{ "code": {code}, }) require.NoError(t, err) @@ -104,13 +108,21 @@ func TestAdminStrategy(t *testing.T) { return ioutilx.MustReadAll(res.Body) } + assertEmailNotVerified := func(t *testing.T, email string) { + addr, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, email) + assert.NoError(t, err) + assert.False(t, addr.Verified) + assert.Nil(t, addr.VerifiedAt) + assert.Equal(t, identity.VerifiableAddressStatusPending, addr.Status) + } + t.Run("description=should create code without email", func(t *testing.T) { id := identity.Identity{Traits: identity.Traits(`{}`)} require.NoError(t, reg.IdentityManager().Create(context.Background(), &id, identity.ManagerAllowWriteProtectedTraits)) - code, _, err := createCode(id.ID.String(), nil) + code, _, err := createCode(createCodeParams{IdentityId: id.ID.String()}) require.NoError(t, err) require.NotEmpty(t, code.RecoveryLink) @@ -119,8 +131,14 @@ func TestAdminStrategy(t *testing.T) { require.NotEmpty(t, code.RecoveryCode) require.True(t, code.ExpiresAt.Before(time.Now().Add(conf.SelfServiceFlowRecoveryRequestLifespan(ctx)))) - body := submitRecoveryLink(t, code.RecoveryLink, code.RecoveryCode) + client := pointerx.Ptr(*publicTS.Client()) + client.Jar, _ = cookiejar.New(nil) + body := submitRecoveryCode(t, client, code.RecoveryLink, code.RecoveryCode) testhelpers.AssertMessage(t, body, "You successfully recovered your account. Please change your password or set up an alternative login method (e.g. social sign in) within the next 60.00 minutes.") + u, err := url.Parse(publicTS.URL) + cs := client.Jar.Cookies(u) + require.Len(t, cs, 1, "%s", body) + assert.Equal(t, "ory_kratos_session", cs[0].Name, "%s", body) }) t.Run("description=should not be able to recover with expired code", func(t *testing.T) { @@ -130,22 +148,18 @@ func TestAdminStrategy(t *testing.T) { require.NoError(t, reg.IdentityManager().Create(context.Background(), &id, identity.ManagerAllowWriteProtectedTraits)) - code, _, err := createCode(id.ID.String(), pointerx.String("100ms")) + code, _, err := createCode(createCodeParams{IdentityId: id.ID.String(), ExpiresIn: pointerx.Ptr("100ms")}) require.NoError(t, err) time.Sleep(time.Millisecond * 100) require.NotEmpty(t, code.RecoveryLink) require.True(t, code.ExpiresAt.Before(time.Now().Add(conf.SelfServiceFlowRecoveryRequestLifespan(ctx)))) - body := submitRecoveryLink(t, code.RecoveryLink, code.RecoveryCode) + body := submitRecoveryCode(t, nil, code.RecoveryLink, code.RecoveryCode) testhelpers.AssertMessage(t, body, "The recovery flow expired 0.00 minutes ago, please try again.") // The recovery address should not be verified if the flow was initiated by the admins - addr, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, recoveryEmail) - assert.NoError(t, err) - assert.False(t, addr.Verified) - assert.Nil(t, addr.VerifiedAt) - assert.Equal(t, identity.VerifiableAddressStatusPending, addr.Status) + assertEmailNotVerified(t, recoveryEmail) }) t.Run("description=should create a valid recovery link and set the expiry time as well and recover the account", func(t *testing.T) { @@ -155,35 +169,32 @@ func TestAdminStrategy(t *testing.T) { require.NoError(t, reg.IdentityManager().Create(context.Background(), &id, identity.ManagerAllowWriteProtectedTraits)) - code, _, err := createCode(id.ID.String(), nil) + code, _, err := createCode(createCodeParams{IdentityId: id.ID.String()}) require.NoError(t, err) require.NotEmpty(t, code.RecoveryLink) require.True(t, code.ExpiresAt.Before(time.Now().Add(conf.SelfServiceFlowRecoveryRequestLifespan(ctx)+time.Second))) - body := submitRecoveryLink(t, code.RecoveryLink, code.RecoveryCode) + body := submitRecoveryCode(t, nil, code.RecoveryLink, code.RecoveryCode) testhelpers.AssertMessage(t, body, "You successfully recovered your account. Please change your password or set up an alternative login method (e.g. social sign in) within the next 60.00 minutes.") - addr, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, recoveryEmail) - assert.NoError(t, err) - assert.False(t, addr.Verified) - assert.Nil(t, addr.VerifiedAt) - assert.Equal(t, identity.VerifiableAddressStatusPending, addr.Status) + // The recovery address should be verified if the flow was initiated by the admins + assertEmailNotVerified(t, recoveryEmail) }) t.Run("case=should not be able to use code from different flow", func(t *testing.T) { email := testhelpers.RandomEmail() i := createIdentityToRecover(t, reg, email) - c1, _, err := createCode(i.ID.String(), pointerx.String("1h")) + c1, _, err := createCode(createCodeParams{IdentityId: i.ID.String(), ExpiresIn: pointerx.Ptr("1h")}) require.NoError(t, err) - c2, _, err := createCode(i.ID.String(), pointerx.String("1h")) + c2, _, err := createCode(createCodeParams{IdentityId: i.ID.String(), ExpiresIn: pointerx.Ptr("1h")}) require.NoError(t, err) code2 := c2.RecoveryCode require.NotEmpty(t, code2) - body := submitRecoveryLink(t, c1.RecoveryLink, c2.RecoveryCode) + body := submitRecoveryCode(t, nil, c1.RecoveryLink, c2.RecoveryCode) testhelpers.AssertMessage(t, body, "The recovery code is invalid or has already been used. Please try again.") }) @@ -192,7 +203,7 @@ func TestAdminStrategy(t *testing.T) { email := testhelpers.RandomEmail() i := createIdentityToRecover(t, reg, email) - c1, _, err := createCode(i.ID.String(), pointerx.String("1h")) + c1, _, err := createCode(createCodeParams{IdentityId: i.ID.String(), ExpiresIn: pointerx.Ptr("1h")}) require.NoError(t, err) res, err := http.Get(c1.RecoveryLink) @@ -201,4 +212,27 @@ func TestAdminStrategy(t *testing.T) { snapshotx.SnapshotT(t, json.RawMessage(gjson.GetBytes(body, "ui.nodes").String())) }) + + t.Run("case=should be able to create and complete an API flow", func(t *testing.T) { + email := testhelpers.RandomEmail() + i := createIdentityToRecover(t, reg, email) + + code, _, err := createCode(createCodeParams{IdentityId: i.ID.String(), FlowType: pointerx.Ptr(string(flow.TypeAPI))}) + require.NoError(t, err) + + res, err := publicTS.Client().Get(code.RecoveryLink) + require.NoError(t, err) + body := ioutilx.MustReadAll(res.Body) + + action := gjson.GetBytes(body, "ui.action").String() + require.NotEmpty(t, action) + + res, err = publicTS.Client().Post(action, "application/json", strings.NewReader(fmt.Sprintf(`{"code":"%s"}`, code.RecoveryCode))) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + + continueWith := gjson.GetBytes(ioutilx.MustReadAll(res.Body), "continue_with").Array() + require.Len(t, continueWith, 2) + assert.EqualValues(t, flow.ContinueWithActionSetOrySessionTokenString, continueWith[0].Get("action").String()) + }) } diff --git a/selfservice/strategy/code/strategy_recovery_test.go b/selfservice/strategy/code/strategy_recovery_test.go index a5b4cef8bca5..483e8dfc6f89 100644 --- a/selfservice/strategy/code/strategy_recovery_test.go +++ b/selfservice/strategy/code/strategy_recovery_test.go @@ -15,6 +15,8 @@ import ( "testing" "time" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/davecgh/go-spew/spew" "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" @@ -30,6 +32,7 @@ import ( "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/hook/hooktest" "github.com/ory/kratos/selfservice/strategy/code" "github.com/ory/kratos/session" "github.com/ory/kratos/text" @@ -293,9 +296,52 @@ func TestRecovery(t *testing.T) { assert.Contains(t, gjson.Get(body, "redirect_browser_to").String(), "settings-ts?") }) + t.Run("description=should pass transient data to email template and webhooks", func(t *testing.T) { + webhookTS := hooktest.NewServer() + t.Cleanup(webhookTS.Close) + + conf.MustSet(ctx, "selfservice.flows.recovery.after.hooks", []config.SelfServiceHook{webhookTS.HookConfig()}) + t.Cleanup(func() { conf.MustSet(ctx, "selfservice.flows.recovery.after.hooks", nil) }) + + client := testhelpers.NewClientWithCookies(t) + email := testhelpers.RandomEmail() + createIdentityToRecover(t, reg, email) + templatePayload := `{"payload":"template data"}` + webhookPayload := `{"payload":"webhook data"}` + + f := testhelpers.InitializeRecoveryFlowViaBrowser(t, client, false, public, nil) + + formPayload := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + formPayload.Set("email", email) + formPayload.Set("transient_payload", templatePayload) + + body, _ := testhelpers.RecoveryMakeRequest(t, false, f, client, formPayload.Encode()) + message := testhelpers.CourierExpectMessage(ctx, t, reg, email, "Recover access to your account") + assert.Equal(t, templatePayload, gjson.GetBytes(message.TemplateData, "transient_payload").String(), + "should pass transient payload to email template") + + recoveryCode := testhelpers.CourierExpectCodeInMessage(t, message, 1) + assert.NotEmpty(t, recoveryCode) + + action := gjson.Get(body, "ui.action").String() + assert.NotEmpty(t, action) + + _, err := client.Post(action, "application/x-www-form-urlencoded", bytes.NewBufferString( + withCSRFToken(t, RecoveryClientTypeBrowser, body, url.Values{ + "code": {recoveryCode}, + "method": {"code"}, + "transient_payload": {webhookPayload}, + }))) + require.NoError(t, err) + + assert.JSONEq(t, webhookPayload, gjson.GetBytes(webhookTS.LastBody, "flow.transient_payload").String(), + "should pass transient payload to webhook") + }) + t.Run("description=should return browser to return url", func(t *testing.T) { returnTo := public.URL + "/return-to" - conf.Set(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnTo}) + conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnTo}) + for _, tc := range []struct { desc string returnTo string @@ -313,9 +359,9 @@ func TestRecovery(t *testing.T) { desc: "should use return_to from config", returnTo: returnTo, f: func(t *testing.T, client *http.Client, identity *identity.Identity) *kratos.RecoveryFlow { - conf.Set(ctx, config.ViperKeySelfServiceRecoveryBrowserDefaultReturnTo, returnTo) + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryBrowserDefaultReturnTo, returnTo) t.Cleanup(func() { - conf.Set(ctx, config.ViperKeySelfServiceRecoveryBrowserDefaultReturnTo, "") + conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryBrowserDefaultReturnTo, "") }) return testhelpers.InitializeRecoveryFlowViaBrowser(t, client, false, public, nil) }, @@ -331,10 +377,10 @@ func TestRecovery(t *testing.T) { desc: "should use return_to with an account that has 2fa enabled", returnTo: returnTo, f: func(t *testing.T, client *http.Client, id *identity.Identity) *kratos.RecoveryFlow { - conf.Set(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL) - conf.Set(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL) - conf.Set(ctx, config.ViperKeyWebAuthnRPDisplayName, "Kratos") - conf.Set(ctx, config.ViperKeyWebAuthnRPID, "ory.sh") + conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL) + conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, config.HighestAvailableAAL) + conf.MustSet(ctx, config.ViperKeyWebAuthnRPDisplayName, "Kratos") + conf.MustSet(ctx, config.ViperKeyWebAuthnRPID, "ory.sh") t.Cleanup(func() { conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, identity.AuthenticatorAssuranceLevel1) @@ -476,10 +522,10 @@ func TestRecovery(t *testing.T) { } req := httptest.NewRequest("GET", "/sessions/whoami", nil) - session, err := session.NewActiveSession( - req, - &identity.Identity{ID: x.NewUUID(), State: identity.StateActive}, - testhelpers.NewSessionLifespanProvider(time.Hour), + req.WithContext(confighelpers.WithConfigValue(ctx, config.ViperKeySessionLifespan, time.Hour)) + session, err := testhelpers.NewActiveSession(req, + reg, + &identity.Identity{ID: x.NewUUID(), State: identity.StateActive, NID: x.NewUUID()}, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1, @@ -488,7 +534,7 @@ func TestRecovery(t *testing.T) { require.NoError(t, err) // Add the authentication to the request - client.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewAuthorizedTransport(t, reg, session), t).RoundTripper + client.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewAuthorizedTransport(t, ctx, reg, session), t).RoundTripper v := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) v.Set("email", "some-email@example.org") @@ -588,7 +634,7 @@ func TestRecovery(t *testing.T) { id := createIdentityToRecover(t, reg, email) req := httptest.NewRequest("GET", "/sessions/whoami", nil) - sess, err := session.NewActiveSession(req, id, conf, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + sess, err := testhelpers.NewActiveSession(req, reg, id, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) require.NoError(t, err) require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), sess)) @@ -680,7 +726,7 @@ func TestRecovery(t *testing.T) { rs, res, err := testhelpers. NewSDKCustomClient(public, c). - FrontendApi.GetRecoveryFlow(context.Background()). + FrontendAPI.GetRecoveryFlow(context.Background()). Id(flowId). Execute() @@ -1316,11 +1362,12 @@ func TestRecovery_WithContinueWith(t *testing.T) { f = testhelpers.InitializeRecoveryFlowViaBrowser(t, client, isSPA, public, nil) } req := httptest.NewRequest("GET", "/sessions/whoami", nil) + req.WithContext(confighelpers.WithConfigValue(ctx, config.ViperKeySessionLifespan, time.Hour)) - session, err := session.NewActiveSession( + session, err := testhelpers.NewActiveSession( req, - &identity.Identity{ID: x.NewUUID(), State: identity.StateActive}, - testhelpers.NewSessionLifespanProvider(time.Hour), + reg, + &identity.Identity{ID: x.NewUUID(), State: identity.StateActive, NID: x.NewUUID()}, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1, @@ -1329,7 +1376,7 @@ func TestRecovery_WithContinueWith(t *testing.T) { require.NoError(t, err) // Add the authentication to the request - client.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewAuthorizedTransport(t, reg, session), t).RoundTripper + client.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewAuthorizedTransport(t, ctx, reg, session), t).RoundTripper v := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) v.Set("email", "some-email@example.org") @@ -1419,7 +1466,7 @@ func TestRecovery_WithContinueWith(t *testing.T) { email := testhelpers.RandomEmail() id := createIdentityToRecover(t, reg, email) - otherSession, err := session.NewActiveSession(httptest.NewRequest("GET", "/sessions/whoami", nil), id, conf, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + otherSession, err := testhelpers.NewActiveSession(httptest.NewRequest("GET", "/sessions/whoami", nil), reg, id, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) require.NoError(t, err) require.NoError(t, reg.SessionPersister().UpsertSession(ctx, otherSession)) @@ -1527,7 +1574,7 @@ func TestRecovery_WithContinueWith(t *testing.T) { rs, res, err := testhelpers. NewSDKCustomClient(public, c). - FrontendApi.GetRecoveryFlow(context.Background()). + FrontendAPI.GetRecoveryFlow(context.Background()). Id(flowId). Execute() @@ -1928,7 +1975,7 @@ func TestDisabledStrategy(t *testing.T) { require.NoError(t, reg.IdentityManager().Create(context.Background(), &id, identity.ManagerAllowWriteProtectedTraits)) - rl, _, err := adminSDK.IdentityApi. + rl, _, err := adminSDK.IdentityAPI. CreateRecoveryLinkForIdentity(context.Background()). CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{IdentityId: id.ID.String()}). Execute() diff --git a/selfservice/strategy/code/strategy_registration.go b/selfservice/strategy/code/strategy_registration.go index 4d728426348f..da53cdfb2cd9 100644 --- a/selfservice/strategy/code/strategy_registration.go +++ b/selfservice/strategy/code/strategy_registration.go @@ -5,9 +5,11 @@ package code import ( "context" - "database/sql" "encoding/json" "net/http" + "strings" + + "github.com/tidwall/gjson" "github.com/ory/herodot" "github.com/ory/x/otelx" @@ -50,15 +52,15 @@ type updateRegistrationFlowWithCodeMethod struct { // required: true Method string `json:"method" form:"method"` - // Transient data to pass along to any webhooks + // Resend restarts the flow with a new code // // required: false - TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` + Resend string `json:"resend" form:"resend"` - // Resend restarts the flow with a new code + // Transient data to pass along to any webhooks // // required: false - Resend string `json:"resend" form:"resend"` + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (p *updateRegistrationFlowWithCodeMethod) GetResend() string { @@ -91,30 +93,10 @@ func (s *Strategy) PopulateRegistrationMethod(r *http.Request, rf *registration. return s.PopulateMethod(r, rf) } -type options func(*identity.Identity) error - -func WithCredentials(via identity.CodeAddressType, usedAt sql.NullTime) options { - return func(i *identity.Identity) error { - return i.SetCredentialsWithConfig(identity.CredentialsTypeCodeAuth, identity.Credentials{Type: identity.CredentialsTypePassword, Identifiers: []string{}}, &identity.CredentialsCode{AddressType: via, UsedAt: usedAt}) - } -} - -func (s *Strategy) handleIdentityTraits(ctx context.Context, f *registration.Flow, traits json.RawMessage, transientPayload json.RawMessage, i *identity.Identity, opts ...options) error { - f.TransientPayload = transientPayload - if len(traits) == 0 { - traits = json.RawMessage("{}") - } - - // we explicitly set the Code credentials type - i.Traits = identity.Traits(traits) - if err := i.SetCredentialsWithConfig(s.ID(), identity.Credentials{Type: s.ID(), Identifiers: []string{}}, &identity.CredentialsCode{UsedAt: sql.NullTime{}}); err != nil { - return err - } - - for _, opt := range opts { - if err := opt(i); err != nil { - return err - } +func (s *Strategy) validateTraits(ctx context.Context, traits json.RawMessage, i *identity.Identity) error { + i.Traits = []byte("{}") + if gjson.ValidBytes(traits) { + i.Traits = identity.Traits(traits) } // Validate the identity @@ -125,22 +107,30 @@ func (s *Strategy) handleIdentityTraits(ctx context.Context, f *registration.Flo return nil } -func (s *Strategy) getCredentialsFromTraits(ctx context.Context, f *registration.Flow, i *identity.Identity, traits, transientPayload json.RawMessage) (*identity.Credentials, error) { - if err := s.handleIdentityTraits(ctx, f, traits, transientPayload, i); err != nil { - return nil, errors.WithStack(err) +func (s *Strategy) validateAndGetCredentialsFromTraits(ctx context.Context, i *identity.Identity, traits json.RawMessage) (*identity.Credentials, *identity.CredentialsCode, error) { + if err := s.validateTraits(ctx, traits, i); err != nil { + return nil, nil, errors.WithStack(err) } cred, ok := i.GetCredentials(identity.CredentialsTypeCodeAuth) if !ok { - return nil, errors.WithStack(schema.NewMissingIdentifierError()) - } else if len(cred.Identifiers) == 0 { - return nil, errors.WithStack(schema.NewMissingIdentifierError()) + return nil, nil, errors.WithStack(schema.NewMissingIdentifierError()) + } else if len(strings.Join(cred.Identifiers, "")) == 0 { + return nil, nil, errors.WithStack(schema.NewMissingIdentifierError()) } - return cred, nil + + var conf identity.CredentialsCode + if len(cred.Config) > 0 { + if err := json.Unmarshal(cred.Config, &conf); err != nil { + return nil, nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to unmarshal credentials config: %s", err)) + } + } + + return cred, &conf, nil } func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registration.Flow, i *identity.Identity) (err error) { - ctx, span := s.deps.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.code.strategy.Register") + ctx, span := s.deps.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.code.Strategy.Register") defer otelx.End(span, &err) if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.deps); err != nil { @@ -152,6 +142,8 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat return s.HandleRegistrationError(ctx, r, f, &p, err) } + f.TransientPayload = p.TransientPayload + if err := flow.EnsureCSRF(s.deps, r, f.Type, s.deps.Config().DisableAPIFlowEnforcement(ctx), s.deps.GenerateCSRFToken, p.CSRFToken); err != nil { return s.HandleRegistrationError(ctx, r, f, &p, err) } @@ -182,7 +174,7 @@ func (s *Strategy) registrationSendEmail(ctx context.Context, w http.ResponseWri // Create the Registration code // Step 1: validate the identity's traits - cred, err := s.getCredentialsFromTraits(ctx, f, i, p.Traits, p.TransientPayload) + _, conf, err := s.validateAndGetCredentialsFromTraits(ctx, i, p.Traits) if err != nil { return err } @@ -194,9 +186,10 @@ func (s *Strategy) registrationSendEmail(ctx context.Context, w http.ResponseWri // Step 3: Get the identity email and send the code var addresses []Address - for _, identifier := range cred.Identifiers { - addresses = append(addresses, Address{To: identifier, Via: identity.AddressTypeEmail}) + for _, address := range conf.Addresses { + addresses = append(addresses, Address{To: address.Address, Via: address.Channel}) } + // kratos only supports `email` identifiers at the moment with the code method // this is validated in the identity validation step above if err := s.deps.CodeSender().SendCode(ctx, f, i, addresses...); err != nil { @@ -244,7 +237,7 @@ func (s *Strategy) registrationVerifyCode(ctx context.Context, f *registration.F // Step 1: Re-validate the identity's traits // this is important since the client could have switched out the identity's traits // this method also returns the credentials for a temporary identity - cred, err := s.getCredentialsFromTraits(ctx, f, i, p.Traits, p.TransientPayload) + cred, _, err := s.validateAndGetCredentialsFromTraits(ctx, i, p.Traits) if err != nil { return err } @@ -265,9 +258,12 @@ func (s *Strategy) registrationVerifyCode(ctx context.Context, f *registration.F return errors.WithStack(err) } - // Step 4: The code was correct, populate the Identity credentials and traits - if err := s.handleIdentityTraits(ctx, f, p.Traits, p.TransientPayload, i, WithCredentials(registrationCode.AddressType, registrationCode.UsedAt)); err != nil { - return errors.WithStack(err) + // Step 4: Verify the address + if err := s.verifyAddress(ctx, i, Address{ + To: registrationCode.Address, + Via: registrationCode.AddressType, + }); err != nil { + return err } // since nothing has errored yet, we can assume that the code is correct diff --git a/selfservice/strategy/code/strategy_registration_test.go b/selfservice/strategy/code/strategy_registration_test.go index 1ca150b5f0ba..c5988047d0a3 100644 --- a/selfservice/strategy/code/strategy_registration_test.go +++ b/selfservice/strategy/code/strategy_registration_test.go @@ -5,6 +5,7 @@ package code_test import ( "context" + _ "embed" "encoding/json" "fmt" "io" @@ -14,7 +15,7 @@ import ( "strings" "testing" - _ "embed" + "github.com/ory/kratos/selfservice/flow" "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" @@ -38,6 +39,7 @@ type state struct { email string testServer *httptest.Server resultIdentity *identity.Identity + body string } func TestRegistrationCodeStrategyDisabled(t *testing.T) { @@ -47,6 +49,7 @@ func TestRegistrationCodeStrategyDisabled(t *testing.T) { conf.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypePassword.String()), false) conf.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth.String()), false) conf.MustSet(ctx, fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth), false) + conf.MustSet(ctx, "selfservice.flows.registration.enable_legacy_one_step", true) _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) _ = testhelpers.NewErrorTestServer(t, reg) @@ -103,6 +106,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".code.hooks", []map[string]interface{}{ {"hook": "session"}, }) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, true) _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) _ = testhelpers.NewErrorTestServer(t, reg) @@ -161,7 +165,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { s.email = testhelpers.RandomEmail() } - rf, resp, err := testhelpers.NewSDKCustomClient(s.testServer, s.client).FrontendApi.GetRegistrationFlow(context.Background()).Id(s.flowID).Execute() + rf, resp, err := testhelpers.NewSDKCustomClient(s.testServer, s.client).FrontendAPI.GetRegistrationFlow(context.Background()).Id(s.flowID).Execute() require.NoError(t, err) require.EqualValues(t, http.StatusOK, resp.StatusCode) @@ -171,6 +175,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { values.Set("method", "code") body, resp := testhelpers.RegistrationMakeRequest(t, apiType == ApiTypeNative, apiType == ApiTypeSPA, rf, s.client, testhelpers.EncodeFormAsJSON(t, apiType == ApiTypeNative, values)) + s.body = body if submitAssertion != nil { submitAssertion(ctx, t, s, body, resp) @@ -198,7 +203,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { submitOTP := func(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, s *state, vals func(v *url.Values), apiType ApiType, submitAssertion onSubmitAssertion) *state { t.Helper() - rf, resp, err := testhelpers.NewSDKCustomClient(s.testServer, s.client).FrontendApi.GetRegistrationFlow(context.Background()).Id(s.flowID).Execute() + rf, resp, err := testhelpers.NewSDKCustomClient(s.testServer, s.client).FrontendAPI.GetRegistrationFlow(context.Background()).Id(s.flowID).Execute() require.NoError(t, err) require.EqualValues(t, http.StatusOK, resp.StatusCode) @@ -212,13 +217,14 @@ func TestRegistrationCodeStrategy(t *testing.T) { vals(&values) body, resp := testhelpers.RegistrationMakeRequest(t, apiType == ApiTypeNative, apiType == ApiTypeSPA, rf, s.client, testhelpers.EncodeFormAsJSON(t, apiType == ApiTypeNative, values)) + s.body = body if submitAssertion != nil { submitAssertion(ctx, t, s, body, resp) return s } - require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, http.StatusOK, resp.StatusCode, body) verifiableAddress, err := reg.PrivilegedIdentityPool().FindVerifiableAddressByValue(ctx, identity.VerifiableAddressTypeEmail, s.email) require.NoError(t, err) @@ -239,7 +245,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { t.Parallel() ctx := context.Background() - _, reg, public := setup(ctx, t) + conf, reg, public := setup(ctx, t) for _, tc := range []struct { d string @@ -278,6 +284,15 @@ func TestRegistrationCodeStrategy(t *testing.T) { state = submitOTP(ctx, t, reg, state, func(v *url.Values) { v.Set("code", registrationCode) }, tc.apiType, nil) + + if tc.apiType == ApiTypeSPA { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(state.body, "continue_with.0.action").String(), "%s", state.body) + assert.Contains(t, gjson.Get(state.body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", state.body) + } else if tc.apiType == ApiTypeSPA { + assert.Empty(t, gjson.Get(state.body, "continue_with").Array(), "%s", state.body) + } else if tc.apiType == ApiTypeNative { + assert.NotContains(t, gjson.Get(state.body, "continue_with").Raw, string(flow.ContinueWithActionRedirectBrowserToString), "%s", state.body) + } }) t.Run("case=should normalize email address on sign up", func(t *testing.T) { @@ -333,7 +348,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { require.NotEmpty(t, attr) val := gjson.Get(attr, "#(attributes.type==hidden).attributes.value").String() - require.Equal(t, "code", val) + require.Equal(t, "code", val, body) }) message := testhelpers.CourierExpectMessage(ctx, t, reg, s.email, "Complete your account registration") @@ -511,8 +526,11 @@ func TestRegistrationCodeStrategy(t *testing.T) { } { t.Run("test="+tc.d, func(t *testing.T) { t.Run("case=should fail when schema does not contain the `code` extension", func(t *testing.T) { - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/default.schema.json") + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/no-code.schema.json") + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, false) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeyCodeConfigMissingCredentialFallbackEnabled, true) testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/code.identity.schema.json") }) @@ -525,7 +543,7 @@ func TestRegistrationCodeStrategy(t *testing.T) { // we expect a redirect to the registration page with the flow id require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, conf.SelfServiceFlowRegistrationUI(ctx).Path, resp.Request.URL.Path) - rf, resp, err := testhelpers.NewSDKCustomClient(public, s.client).FrontendApi.GetRegistrationFlow(ctx).Id(resp.Request.URL.Query().Get("flow")).Execute() + rf, resp, err := testhelpers.NewSDKCustomClient(public, s.client).FrontendAPI.GetRegistrationFlow(ctx).Id(resp.Request.URL.Query().Get("flow")).Execute() require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) body, err := json.Marshal(rf) diff --git a/selfservice/strategy/code/strategy_test.go b/selfservice/strategy/code/strategy_test.go index 4561a280fb96..5edbef5ec718 100644 --- a/selfservice/strategy/code/strategy_test.go +++ b/selfservice/strategy/code/strategy_test.go @@ -5,8 +5,14 @@ package code_test import ( "context" + "fmt" "testing" + "github.com/stretchr/testify/require" + + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/kratos/internal" + "github.com/stretchr/testify/assert" "github.com/ory/kratos/internal/testhelpers" @@ -74,3 +80,123 @@ func TestMaskAddress(t *testing.T) { }) } } + +func TestCountActiveCredentials(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + strategy := code.NewStrategy(reg) + ctx := context.Background() + + t.Run("first factor", func(t *testing.T) { + for k, tc := range []struct { + in map[identity.CredentialsType]identity.Credentials + expected int + passwordlessEnabled bool + enabled bool + }{ + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte{}, + }}, + passwordlessEnabled: false, + enabled: true, + expected: 0, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte{}, + }}, + passwordlessEnabled: true, + enabled: false, + expected: 1, + }, + { + in: map[identity.CredentialsType]identity.Credentials{}, + passwordlessEnabled: true, + enabled: true, + expected: 1, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte(`{}`), + }}, + passwordlessEnabled: true, + enabled: true, + expected: 1, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + ctx := confighelpers.WithConfigValue(ctx, "selfservice.methods.code.passwordless_enabled", tc.passwordlessEnabled) + ctx = confighelpers.WithConfigValue(ctx, "selfservice.methods.code.enabled", tc.enabled) + + cc := map[identity.CredentialsType]identity.Credentials{} + for _, c := range tc.in { + cc[c.Type] = c + } + + actual, err := strategy.CountActiveFirstFactorCredentials(ctx, cc) + require.NoError(t, err) + assert.Equal(t, tc.expected, actual) + }) + } + }) + + t.Run("second factor", func(t *testing.T) { + for k, tc := range []struct { + in map[identity.CredentialsType]identity.Credentials + expected int + mfaEnabled bool + enabled bool + }{ + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte{}, + }}, + mfaEnabled: false, + enabled: true, + expected: 0, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte{}, + }}, + mfaEnabled: true, + enabled: false, + expected: 1, + }, + { + in: map[identity.CredentialsType]identity.Credentials{}, + mfaEnabled: true, + enabled: true, + expected: 1, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte(`{}`), + }}, + mfaEnabled: true, + enabled: true, + expected: 1, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + ctx := confighelpers.WithConfigValue(ctx, "selfservice.methods.code.mfa_enabled", tc.mfaEnabled) + ctx = confighelpers.WithConfigValue(ctx, "selfservice.methods.code.enabled", tc.enabled) + + cc := map[identity.CredentialsType]identity.Credentials{} + for _, c := range tc.in { + cc[c.Type] = c + } + + actual, err := strategy.CountActiveMultiFactorCredentials(ctx, cc) + require.NoError(t, err) + assert.Equal(t, tc.expected, actual) + }) + } + }) +} diff --git a/selfservice/strategy/code/strategy_verification.go b/selfservice/strategy/code/strategy_verification.go index e6969fda738d..4a41991e2a68 100644 --- a/selfservice/strategy/code/strategy_verification.go +++ b/selfservice/strategy/code/strategy_verification.go @@ -5,6 +5,7 @@ package code import ( "context" + "encoding/json" "net/http" "time" @@ -65,11 +66,15 @@ func (s *Strategy) decodeVerification(r *http.Request) (*updateVerificationFlowW } // handleVerificationError is a convenience function for handling all types of errors that may occur (e.g. validation error). -func (s *Strategy) handleVerificationError(w http.ResponseWriter, r *http.Request, f *verification.Flow, body *updateVerificationFlowWithCodeMethod, err error) error { +func (s *Strategy) handleVerificationError(r *http.Request, f *verification.Flow, body *updateVerificationFlowWithCodeMethod, err error) error { if f != nil { f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) + email := "" + if body != nil { + email = body.Email + } f.UI.GetNodes().Upsert( - node.NewInputField("email", body.Email, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), + node.NewInputField("email", email, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), ) } @@ -110,6 +115,11 @@ type updateVerificationFlowWithCodeMethod struct { // The id of the flow Flow string `json:"-" form:"-"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } // getMethod returns the method of this submission or "" if no method could be found @@ -125,21 +135,23 @@ func (body *updateVerificationFlowWithCodeMethod) getMethod() verification.Verif } func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verification.Flow) (err error) { - ctx, span := s.deps.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.code.strategy.Verify") + ctx, span := s.deps.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.code.Strategy.Verify") span.SetAttributes(attribute.String("selfservice_flows_verification_use", s.deps.Config().SelfServiceFlowVerificationUse(ctx))) defer otelx.End(span, &err) body, err := s.decodeVerification(r) if err != nil { - return s.handleVerificationError(w, r, nil, body, err) + return s.handleVerificationError(r, nil, body, err) } + f.TransientPayload = body.TransientPayload + if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.VerificationStrategyID(), string(body.getMethod()), s.deps); err != nil { - return s.handleVerificationError(w, r, f, body, err) + return s.handleVerificationError(r, f, body, err) } if err := f.Valid(); err != nil { - return s.handleVerificationError(w, r, f, body, err) + return s.handleVerificationError(r, f, body, err) } switch f.State { @@ -189,20 +201,20 @@ func (s *Strategy) verificationHandleFormSubmission(w http.ResponseWriter, r *ht return s.verificationUseCode(w, r, body.Code, f) } else if len(body.Email) == 0 { // If no code and no email was provided, fail with a validation error - return s.handleVerificationError(w, r, f, body, schema.NewRequiredError("#/email", "email")) + return s.handleVerificationError(r, f, body, schema.NewRequiredError("#/email", "email")) } if err := flow.EnsureCSRF(s.deps, r, f.Type, s.deps.Config().DisableAPIFlowEnforcement(r.Context()), s.deps.GenerateCSRFToken, body.CSRFToken); err != nil { - return s.handleVerificationError(w, r, f, body, err) + return s.handleVerificationError(r, f, body, err) } if err := s.deps.VerificationCodePersister().DeleteVerificationCodesOfFlow(r.Context(), f.ID); err != nil { - return s.handleVerificationError(w, r, f, body, err) + return s.handleVerificationError(r, f, body, err) } if err := s.deps.CodeSender().SendVerificationCode(r.Context(), f, identity.VerifiableAddressTypeEmail, body.Email); err != nil { if !errors.Is(err, ErrUnknownAddress) { - return s.handleVerificationError(w, r, f, body, err) + return s.handleVerificationError(r, f, body, err) } // Continue execution } @@ -210,7 +222,7 @@ func (s *Strategy) verificationHandleFormSubmission(w http.ResponseWriter, r *ht f.State = flow.StateEmailSent if err := s.PopulateVerificationMethod(r, f); err != nil { - return s.handleVerificationError(w, r, f, body, err) + return s.handleVerificationError(r, f, body, err) } if body.Email != "" { @@ -221,7 +233,7 @@ func (s *Strategy) verificationHandleFormSubmission(w http.ResponseWriter, r *ht } if err := s.deps.VerificationFlowPersister().UpdateVerificationFlow(r.Context(), f); err != nil { - return s.handleVerificationError(w, r, f, body, err) + return s.handleVerificationError(r, f, body, err) } return nil @@ -246,15 +258,6 @@ func (s *Strategy) verificationUseCode(w http.ResponseWriter, r *http.Request, c return s.retryVerificationFlowWithError(w, r, f.Type, err) } - i, err := s.deps.IdentityPool().GetIdentity(r.Context(), code.VerifiableAddress.IdentityID, identity.ExpandDefault) - if err != nil { - return s.retryVerificationFlowWithError(w, r, f.Type, err) - } - - if err := s.deps.VerificationExecutor().PostVerificationHook(w, r, f, i); err != nil { - return s.retryVerificationFlowWithError(w, r, f.Type, err) - } - address := code.VerifiableAddress address.Verified = true verifiedAt := sqlxx.NullTime(time.Now().UTC()) @@ -264,6 +267,11 @@ func (s *Strategy) verificationUseCode(w http.ResponseWriter, r *http.Request, c return s.retryVerificationFlowWithError(w, r, f.Type, err) } + i, err := s.deps.IdentityPool().GetIdentity(r.Context(), code.VerifiableAddress.IdentityID, identity.ExpandDefault) + if err != nil { + return s.retryVerificationFlowWithError(w, r, f.Type, err) + } + returnTo := f.ContinueURL(r.Context(), s.deps.Config()) f.UI = &container.Container{ @@ -284,6 +292,10 @@ func (s *Strategy) verificationUseCode(w http.ResponseWriter, r *http.Request, c return s.retryVerificationFlowWithError(w, r, flow.TypeBrowser, err) } + if err := s.deps.VerificationExecutor().PostVerificationHook(w, r, f, i); err != nil { + return s.retryVerificationFlowWithError(w, r, f.Type, err) + } + return nil } @@ -297,13 +309,13 @@ func (s *Strategy) retryVerificationFlowWithMessage(w http.ResponseWriter, r *ht f, err := verification.NewFlow(s.deps.Config(), s.deps.Config().SelfServiceFlowVerificationRequestLifespan(r.Context()), s.deps.CSRFHandler().RegenerateToken(w, r), r, s, ft) if err != nil { - return s.handleVerificationError(w, r, f, nil, err) + return s.handleVerificationError(r, f, nil, err) } f.UI.Messages.Add(message) if err := s.deps.VerificationFlowPersister().CreateVerificationFlow(r.Context(), f); err != nil { - return s.handleVerificationError(w, r, f, nil, err) + return s.handleVerificationError(r, f, nil, err) } if x.IsJSONRequest(r) { @@ -325,7 +337,7 @@ func (s *Strategy) retryVerificationFlowWithError(w http.ResponseWriter, r *http f, err := verification.NewFlow(s.deps.Config(), s.deps.Config().SelfServiceFlowVerificationRequestLifespan(r.Context()), s.deps.CSRFHandler().RegenerateToken(w, r), r, s, ft) if err != nil { - return s.handleVerificationError(w, r, f, nil, err) + return s.handleVerificationError(r, f, nil, err) } var toReturn error @@ -338,7 +350,7 @@ func (s *Strategy) retryVerificationFlowWithError(w http.ResponseWriter, r *http } if err := s.deps.VerificationFlowPersister().CreateVerificationFlow(r.Context(), f); err != nil { - return s.handleVerificationError(w, r, f, nil, err) + return s.handleVerificationError(r, f, nil, err) } if x.IsJSONRequest(r) { diff --git a/selfservice/strategy/code/strategy_verification_test.go b/selfservice/strategy/code/strategy_verification_test.go index 6f25e324d8b2..322dc56eabd2 100644 --- a/selfservice/strategy/code/strategy_verification_test.go +++ b/selfservice/strategy/code/strategy_verification_test.go @@ -13,6 +13,7 @@ import ( "net/http/httptest" "net/url" "strings" + "sync" "testing" "time" @@ -297,6 +298,13 @@ func TestVerification(t *testing.T) { }) t.Run("description=should verify an email address", func(t *testing.T) { + var wg sync.WaitGroup + testhelpers.NewVerifyAfterHookWebHookTarget(ctx, t, conf, func(t *testing.T, msg []byte) { + defer wg.Done() + assert.EqualValues(t, true, gjson.GetBytes(msg, "identity.verifiable_addresses.0.verified").Bool(), string(msg)) + assert.EqualValues(t, "completed", gjson.GetBytes(msg, "identity.verifiable_addresses.0.status").String(), string(msg)) + }) + check := func(t *testing.T, actual string) { assert.EqualValues(t, string(node.CodeGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, verificationEmail, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual) @@ -342,15 +350,21 @@ func TestVerification(t *testing.T) { } t.Run("type=browser", func(t *testing.T) { + wg.Add(1) check(t, expectSuccess(t, nil, false, false, values)) + wg.Wait() }) t.Run("type=spa", func(t *testing.T) { + wg.Add(1) check(t, expectSuccess(t, nil, false, true, values)) + wg.Wait() }) t.Run("type=api", func(t *testing.T) { + wg.Add(1) check(t, expectSuccess(t, nil, true, false, values)) + wg.Wait() }) }) diff --git a/selfservice/strategy/code/stub/code-mfa.identity.schema.json b/selfservice/strategy/code/stub/code-mfa.identity.schema.json new file mode 100644 index 000000000000..9ccdaa4cc8ed --- /dev/null +++ b/selfservice/strategy/code/stub/code-mfa.identity.schema.json @@ -0,0 +1,50 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email1": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "email" + } + } + } + }, + "email2": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "email" + } + } + } + }, + "phone1": { + "type": "string", + "format": "tel", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "sms" + } + } + } + } + }, + "required": [] + } + } +} diff --git a/selfservice/strategy/code/stub/code.identity.schema.json b/selfservice/strategy/code/stub/code.identity.schema.json index a7a4e4448442..ecbeb33574c5 100644 --- a/selfservice/strategy/code/stub/code.identity.schema.json +++ b/selfservice/strategy/code/stub/code.identity.schema.json @@ -58,13 +58,29 @@ } } }, + "phone_1": { + "type": "string", + "format": "tel", + "title": "Phone", + "ory.sh/kratos": { + "credentials": { + "code": { + "identifier": true, + "via": "sms" + } + }, + "verification": { + "via": "sms" + } + } + }, "tos": { "type": "boolean", "title": "Tos", "description": "Please accept the terms and conditions" } }, - "required": ["email", "tos"] + "required": [] } } } diff --git a/selfservice/strategy/code/stub/default.schema.json b/selfservice/strategy/code/stub/default.schema.json index 8dc923266050..f13da2b4d1da 100644 --- a/selfservice/strategy/code/stub/default.schema.json +++ b/selfservice/strategy/code/stub/default.schema.json @@ -13,6 +13,10 @@ "credentials": { "password": { "identifier": true + }, + "code": { + "identifier": true, + "via": "email" } }, "verification": { diff --git a/selfservice/strategy/code/stub/no-code-id.schema.json b/selfservice/strategy/code/stub/no-code-id.schema.json new file mode 100644 index 000000000000..3d2dee0d0bd0 --- /dev/null +++ b/selfservice/strategy/code/stub/no-code-id.schema.json @@ -0,0 +1,26 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + } + } + } + } +} diff --git a/selfservice/strategy/code/stub/no-code.schema.json b/selfservice/strategy/code/stub/no-code.schema.json new file mode 100644 index 000000000000..8dc923266050 --- /dev/null +++ b/selfservice/strategy/code/stub/no-code.schema.json @@ -0,0 +1,29 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + } + } + } + } +} diff --git a/selfservice/strategy/code/test/persistence.go b/selfservice/strategy/code/test/persistence.go index f3c120402ddb..d7600e9fbd9a 100644 --- a/selfservice/strategy/code/test/persistence.go +++ b/selfservice/strategy/code/test/persistence.go @@ -8,6 +8,8 @@ import ( "testing" "time" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" "github.com/ory/kratos/selfservice/flow" @@ -24,15 +26,14 @@ import ( "github.com/ory/kratos/x" ) -func TestPersister(ctx context.Context, conf *config.Config, p interface { +func TestPersister(ctx context.Context, p interface { persistence.Persister }, ) func(t *testing.T) { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") - conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) + ctx := confighelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) t.Run("code=recovery", func(t *testing.T) { newRecoveryCodeDTO := func(t *testing.T, email string) (*code.CreateRecoveryCodeParams, *recovery.Flow, *identity.RecoveryAddress) { @@ -51,7 +52,7 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { require.NoError(t, p.CreateIdentity(ctx, &i)) return &code.CreateRecoveryCodeParams{ - RawCode: string(randx.MustString(8, randx.Numeric)), + RawCode: randx.MustString(8, randx.Numeric), FlowID: f.ID, RecoveryAddress: &i.RecoveryAddresses[0], ExpiresIn: time.Minute, diff --git a/selfservice/strategy/idfirst/.schema/login.schema.json b/selfservice/strategy/idfirst/.schema/login.schema.json new file mode 100644 index 000000000000..9e8be40eff58 --- /dev/null +++ b/selfservice/strategy/idfirst/.schema/login.schema.json @@ -0,0 +1,21 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/identity_disovery/login.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "identifier": { + "type": "string", + "minLength": 1 + }, + "method": { + "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + } + } +} diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_password.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_password.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_password.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_password.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_password.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_password.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json new file mode 100644 index 000000000000..086d65ade752 --- /dev/null +++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json @@ -0,0 +1,54 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "identifier_first", + "attributes": { + "name": "identifier", + "type": "text", + "value": "", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "identifier_first", + "attributes": { + "name": "method", + "type": "submit", + "value": "identifier_first", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070009, + "text": "Continue", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/idfirst/schema.go b/selfservice/strategy/idfirst/schema.go new file mode 100644 index 000000000000..77a0d37f54d7 --- /dev/null +++ b/selfservice/strategy/idfirst/schema.go @@ -0,0 +1,11 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package idfirst + +import ( + _ "embed" +) + +//go:embed .schema/login.schema.json +var loginSchema []byte diff --git a/selfservice/strategy/idfirst/strategy.go b/selfservice/strategy/idfirst/strategy.go new file mode 100644 index 000000000000..792fff7bed95 --- /dev/null +++ b/selfservice/strategy/idfirst/strategy.go @@ -0,0 +1,69 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package idfirst + +import ( + "context" + + "github.com/go-playground/validator/v10" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/decoderx" +) + +type dependencies interface { + x.LoggingProvider + x.WriterProvider + x.CSRFTokenGeneratorProvider + x.CSRFProvider + x.TracingProvider + + config.Provider + + identity.PrivilegedPoolProvider + login.StrategyProvider + login.FlowPersistenceProvider +} + +type Strategy struct { + d dependencies + v *validator.Validate + hd *decoderx.HTTP +} + +func NewStrategy(d any) *Strategy { + return &Strategy{ + d: d.(dependencies), + v: validator.New(), + hd: decoderx.NewHTTP(), + } +} + +func (s *Strategy) CountActiveFirstFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { + return 0, nil +} + +func (s *Strategy) CountActiveMultiFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { + return 0, nil +} + +func (s *Strategy) ID() identity.CredentialsType { + return identity.CredentialsType(node.IdentifierFirstGroup) +} + +func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.AuthenticationMethod { + return session.AuthenticationMethod{ + Method: s.ID(), + AAL: identity.NoAuthenticatorAssuranceLevel, + } +} + +func (s *Strategy) NodeGroup() node.UiNodeGroup { + return node.IdentifierFirstGroup +} diff --git a/selfservice/strategy/idfirst/strategy_login.go b/selfservice/strategy/idfirst/strategy_login.go new file mode 100644 index 000000000000..17c39fe914d3 --- /dev/null +++ b/selfservice/strategy/idfirst/strategy_login.go @@ -0,0 +1,196 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package idfirst + +import ( + "net/http" + + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/otelx" + + "github.com/ory/kratos/schema" + + "github.com/pkg/errors" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/decoderx" + "github.com/ory/x/sqlcon" +) + +var ( + _ login.FormHydrator = new(Strategy) + _ login.Strategy = new(Strategy) + ErrNoCredentialsFound = errors.New("no credentials found") +) + +func (s *Strategy) handleLoginError(r *http.Request, f *login.Flow, payload updateLoginFlowWithIdentifierFirstMethod, err error) error { + if f != nil { + f.UI.Nodes.SetValueAttribute("identifier", payload.Identifier) + if f.Type == flow.TypeBrowser { + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + } + + return err +} + +func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, _ *session.Session) (_ *identity.Identity, err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.link.strategy.Login") + defer otelx.End(span, &err) + + if !s.d.Config().SelfServiceLoginFlowIdentifierFirstEnabled(ctx) { + span.SetAttributes(attribute.String("not_responsible_reason", "strategy is not enabled")) + return nil, errors.WithStack(flow.ErrStrategyNotResponsible) + } + + if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil { + span.SetAttributes(attribute.String("not_responsible_reason", "requested AAL is not sufficient")) + return nil, err + } + + var p updateLoginFlowWithIdentifierFirstMethod + if err := s.hd.Decode(r, &p, + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.MustHTTPRawJSONSchemaCompiler(loginSchema), + decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { + return nil, s.handleLoginError(r, f, p, err) + } + f.TransientPayload = p.TransientPayload + + if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + return nil, s.handleLoginError(r, f, p, err) + } + + var opts []login.FormHydratorModifier + + // Look up the user by the identifier. + identityHint, err := s.d.PrivilegedIdentityPool().FindIdentityByCredentialIdentifier(ctx, p.Identifier, + // We are dealing with user input -> lookup should be case-insensitive. + false, + ) + if errors.Is(err, sqlcon.ErrNoRows) { + // If the user is not found, we still want to potentially show the UI for some method. That's why we don't exit here. + // We have to mitigate account enumeration. So we continue without setting the identity hint. + // + // This will later be handled by `didPopulate`. + } else if err != nil { + // An error happened during lookup + return nil, s.handleLoginError(r, f, p, err) + } else if !s.d.Config().SecurityAccountEnumerationMitigate(ctx) { + // Hydrate credentials + if err := s.d.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, identityHint, identity.ExpandCredentials); err != nil { + return nil, s.handleLoginError(r, f, p, err) + } + } + + f.UI.ResetMessages() + f.UI.Nodes.SetValueAttribute("identifier", p.Identifier) + + // Add identity hint + opts = append(opts, login.WithIdentityHint(identityHint)) + opts = append(opts, login.WithIdentifier(p.Identifier)) + + didPopulate := false + for _, ls := range s.d.LoginStrategies(ctx) { + populator, ok := ls.(login.FormHydrator) + if !ok { + continue + } + + if err := populator.PopulateLoginMethodIdentifierFirstCredentials(r, f, opts...); errors.Is(err, login.ErrBreakLoginPopulate) { + didPopulate = true + break + } else if errors.Is(err, ErrNoCredentialsFound) { + // This strategy is not responsible for this flow. We do not set didPopulate to true if that happens. + } else if err != nil { + return nil, s.handleLoginError(r, f, p, err) + } else { + didPopulate = true + } + } + + // If no strategy populated, it means that the account (very likely) does not exist. We show a user not found error, + // but only if account enumeration mitigation is disabled. Otherwise, we proceed to render the rest of the form. + if !didPopulate && !s.d.Config().SecurityAccountEnumerationMitigate(ctx) { + return nil, s.handleLoginError(r, f, p, errors.WithStack(schema.NewAccountNotFoundError())) + } + + // We found credentials - hide the identifier. + f.UI.GetNodes().RemoveMatching(node.NewInputField("method", s.ID(), s.NodeGroup(), node.InputAttributeTypeSubmit)) + + // We set the identifier to hidden, so it's still available in the form but not visible to the user. + for k, n := range f.UI.Nodes { + if n.ID() != "identifier" { + continue + } + + attrs, ok := f.UI.Nodes[k].Attributes.(*node.InputAttributes) + if !ok { + continue + } + + attrs.Type = node.InputAttributeTypeHidden + f.UI.Nodes[k].Attributes = attrs + } + + f.Active = s.ID() + if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + return nil, s.handleLoginError(r, f, p, err) + } + + if x.IsJSONRequest(r) { + s.d.Writer().WriteCode(w, r, http.StatusBadRequest, f) + } else { + http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowLoginUI(ctx)).String(), http.StatusSeeOther) + } + + return nil, flow.ErrCompletedByStrategy +} + +func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, f *login.Flow) error { + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) + if err != nil { + return err + } + + f.UI.SetNode(node.NewInputField("identifier", "", s.NodeGroup(), node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel)) + f.UI.GetNodes().Append(node.NewInputField("method", s.ID(), s.NodeGroup(), node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue())) + return nil +} + +func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(_ *http.Request, f *login.Flow, opts ...login.FormHydratorModifier) error { + return ErrNoCredentialsFound +} + +func (s *Strategy) RegisterLoginRoutes(_ *x.RouterPublic) {} diff --git a/selfservice/strategy/idfirst/strategy_login_test.go b/selfservice/strategy/idfirst/strategy_login_test.go new file mode 100644 index 000000000000..d8c31bdeb786 --- /dev/null +++ b/selfservice/strategy/idfirst/strategy_login_test.go @@ -0,0 +1,594 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package idfirst_test + +import ( + "bytes" + "context" + _ "embed" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/ory/kratos/selfservice/strategy/oidc" + + "github.com/ory/kratos/selfservice/strategy/idfirst" + + configtesthelpers "github.com/ory/kratos/driver/config/testhelpers" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" + + kratos "github.com/ory/kratos/internal/httpclient" + "github.com/ory/kratos/text" + "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/urlx" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/ui/node" + "github.com/ory/x/snapshotx" +) + +//go:embed stub/default.schema.json +var loginSchema []byte + +func TestCompleteLogin(t *testing.T) { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + + // We enable the password method to test the identifier first strategy + + // ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{"enabled": true}) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{"enabled": true}) + + // ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceLoginFlowStyle, "identifier_first") + conf.MustSet(ctx, config.ViperKeySelfServiceLoginFlowStyle, "identifier_first") + + router := x.NewRouterPublic() + publicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, x.NewRouterAdmin()) + + errTS := testhelpers.NewErrorTestServer(t, reg) + uiTS := testhelpers.NewLoginUIFlowEchoServer(t, reg) + redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) + + // Overwrite these two: + // ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceErrorUI, errTS.URL+"/error-ts") + conf.MustSet(ctx, config.ViperKeySelfServiceErrorUI, errTS.URL+"/error-ts") + + // ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceLoginUI, uiTS.URL+"/login-ts") + conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, uiTS.URL+"/login-ts") + + // ctx = testhelpers.WithDefaultIdentitySchemaFromRaw(ctx, loginSchema) + testhelpers.SetDefaultIdentitySchemaFromRaw(conf, loginSchema) + + // ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"}) + conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"}) + + //ensureFieldsExist := func(t *testing.T, body []byte) { + // registrationhelpers.CheckFormContent(t, body, "identifier", + // "password", + // "csrf_token") + //} + + apiClient := testhelpers.NewDebugClient(t) + + t.Run("case=should show the error ui because the request payload is malformed", func(t *testing.T) { + t.Run("type=api", func(t *testing.T) { + f := testhelpers.InitializeLoginFlowViaAPIWithContext(t, ctx, apiClient, publicTS, false) + + body, res := testhelpers.LoginMakeRequestWithContext(t, ctx, true, false, f, apiClient, "14=)=!(%)$/ZP()GHIÖ") + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Contains(t, body, `Expected JSON sent in request body to be an object but got: Number`) + }) + + t.Run("type=browser", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false, testhelpers.InitFlowWithContext(ctx)) + + body, res := testhelpers.LoginMakeRequestWithContext(t, ctx, false, false, f, browserClient, "14=)=!(%)$/ZP()GHIÖ") + assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/login-ts") + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "invalid URL escape", "%s", body) + }) + + t.Run("type=spa", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, true, false, false, testhelpers.InitFlowWithContext(ctx)) + + body, res := testhelpers.LoginMakeRequestWithContext(t, ctx, false, true, f, browserClient, "14=)=!(%)$/ZP()GHIÖ") + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "invalid URL escape", "%s", body) + }) + }) + + t.Run("case=should fail because identifier first can not handle AAL2", func(t *testing.T) { + f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false) + + update, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + update.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(context.Background(), update)) + + req, err := http.NewRequest("POST", f.Ui.Action, bytes.NewBufferString(`{"method":"identifier_first"}`)) + require.NoError(t, err) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + + actual, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router.Router, req) + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + assert.Equal(t, text.NewErrorValidationLoginNoStrategyFound().Text, gjson.GetBytes(actual, "ui.messages.0.text").String()) + }) + + t.Run("should return an error because the request does not exist", func(t *testing.T) { + check := func(t *testing.T, actual string) { + assert.Equal(t, int64(http.StatusNotFound), gjson.Get(actual, "code").Int(), "%s", actual) + assert.Equal(t, "Not Found", gjson.Get(actual, "status").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "message").String(), "Unable to locate the resource", "%s", actual) + } + + fakeFlow := &kratos.LoginFlow{ + Ui: kratos.UiContainer{ + Action: publicTS.URL + login.RouteSubmitFlow + "?flow=" + x.NewUUID().String(), + }, + } + + t.Run("type=api", func(t *testing.T) { + actual, res := testhelpers.LoginMakeRequestWithContext(t, ctx, true, false, fakeFlow, apiClient, "{}") + assert.Len(t, res.Cookies(), 0) + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + check(t, gjson.Get(actual, "error").Raw) + }) + + t.Run("type=browser", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + actual, res := testhelpers.LoginMakeRequestWithContext(t, ctx, false, false, fakeFlow, browserClient, "") + assert.Contains(t, res.Request.URL.String(), errTS.URL) + check(t, actual) + }) + + t.Run("type=api", func(t *testing.T) { + actual, res := testhelpers.LoginMakeRequestWithContext(t, ctx, false, true, fakeFlow, apiClient, "{}") + assert.Len(t, res.Cookies(), 0) + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + check(t, gjson.Get(actual, "error").Raw) + }) + }) + + t.Run("case=should return an error because the request is expired", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySelfServiceLoginRequestLifespan, time.Millisecond*30) + conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySelfServiceLoginRequestLifespan, time.Hour) + conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, nil) + }) + + values := url.Values{ + "csrf_token": {x.FakeCSRFToken}, + "identifier": {"identifier"}, + "method": {"identifier_first"}, + } + + t.Run("type=api", func(t *testing.T) { + f := testhelpers.InitializeLoginFlowViaAPIWithContext(t, ctx, apiClient, publicTS, false) + + time.Sleep(time.Millisecond * 60) + actual, res := testhelpers.LoginMakeRequestWithContext(t, ctx, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values)) + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + assert.NotEqual(t, "00000000-0000-0000-0000-000000000000", gjson.Get(actual, "use_flow_id").String()) + assertx.EqualAsJSONExcept(t, flow.NewFlowExpiredError(time.Now()), json.RawMessage(actual), []string{"use_flow_id", "since", "expired_at"}, "expired", "%s", actual) + }) + + t.Run("type=browser", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false) + + time.Sleep(time.Millisecond * 60) + actual, res := testhelpers.LoginMakeRequestWithContext(t, ctx, false, false, f, browserClient, values.Encode()) + assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/login-ts") + assert.NotEqual(t, f.Id, gjson.Get(actual, "id").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), "expired", "%s", actual) + }) + + t.Run("type=SPA", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, true, false, false) + + time.Sleep(time.Millisecond * 60) + actual, res := testhelpers.LoginMakeRequestWithContext(t, ctx, false, true, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values)) + assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow) + assert.NotEqual(t, "00000000-0000-0000-0000-000000000000", gjson.Get(actual, "use_flow_id").String()) + assertx.EqualAsJSONExcept(t, flow.NewFlowExpiredError(time.Now()), json.RawMessage(actual), []string{"use_flow_id", "since", "expired_at"}, "expired", "%s", actual) + }) + }) + + t.Run("case=should have correct CSRF behavior", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, nil) + }) + + values := url.Values{ + "method": {"identifier_first"}, + "csrf_token": {"invalid_token"}, + "identifier": {"login-identifier-csrf-browser"}, + } + + t.Run("case=should fail because of missing CSRF token/type=browser", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false) + + actual, res := testhelpers.LoginMakeRequest(t, false, false, f, browserClient, values.Encode()) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assertx.EqualAsJSON(t, x.ErrInvalidCSRFToken, + json.RawMessage(actual), "%s", actual) + }) + + t.Run("case=should fail because of missing CSRF token/type=spa", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, true, false, false) + + actual, res := testhelpers.LoginMakeRequest(t, false, true, f, browserClient, values.Encode()) + assert.EqualValues(t, http.StatusForbidden, res.StatusCode) + assertx.EqualAsJSON(t, x.ErrInvalidCSRFToken, + json.RawMessage(gjson.Get(actual, "error").Raw), "%s", actual) + }) + + t.Run("case=should pass even without CSRF token/type=api", func(t *testing.T) { + f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false) + + actual, res := testhelpers.LoginMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values)) + assert.EqualValues(t, http.StatusBadRequest, res.StatusCode) + assert.Contains(t, actual, "1010022") + }) + + t.Run("case=should fail with correct CSRF error cause/type=api", func(t *testing.T) { + for k, tc := range []struct { + mod func(http.Header) + exp string + }{ + { + mod: func(h http.Header) { + h.Add("Cookie", "name=bar") + }, + exp: "The HTTP Request Header included the \\\"Cookie\\\" key", + }, + { + mod: func(h http.Header) { + h.Add("Origin", "www.bar.com") + }, + exp: "The HTTP Request Header included the \\\"Origin\\\" key", + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false) + + req := testhelpers.NewRequest(t, true, "POST", f.Ui.Action, bytes.NewBufferString(testhelpers.EncodeFormAsJSON(t, true, values))) + tc.mod(req.Header) + + res, err := apiClient.Do(req) + require.NoError(t, err) + defer res.Body.Close() + + actual := string(ioutilx.MustReadAll(res.Body)) + assert.EqualValues(t, http.StatusBadRequest, res.StatusCode) + assert.Contains(t, actual, tc.exp) + }) + } + }) + }) + + expectValidationError := func(t *testing.T, isAPI, refresh, isSPA bool, values func(url.Values)) string { + return testhelpers.SubmitLoginForm(t, isAPI, nil, publicTS, values, + isSPA, refresh, + testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK), + testhelpers.ExpectURL(isAPI || isSPA, publicTS.URL+login.RouteSubmitFlow, conf.SelfServiceFlowLoginUI(ctx).String())) + } + + t.Run("should return an error because the user does not exist", func(t *testing.T) { + // In this test we check if the account mitigation behaves correctly by enabling all login strategies EXCEPT + // for the passwordless code strategy. That is because this strategy always shows the login button. + + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true) + + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeOIDC.String(), true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".config", &oidc.ConfigurationCollection{Providers: []oidc.Configuration{ + { + ID: "google", + Provider: "google", + Label: "Google", + ClientID: "a", + ClientSecret: "b", + Mapper: "file://", + }, + }}) + + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeWebAuthn.String(), true) + conf.MustSet(ctx, config.ViperKeyWebAuthnPasswordless, true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.display_name", "Ory Corp") + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.id", "localhost") + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.origin", "http://localhost:4455") + + testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePasskey.String(), true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".enabled", true) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".config.rp.display_name", "Ory Corp") + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".config.rp.id", "localhost") + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".config.rp.origins", []string{"http://localhost:4455"}) + + t.Cleanup(func() { + conf.MustSet(ctx, "selfservice.methods.password", nil) + conf.MustSet(ctx, "selfservice.methods.oidc", nil) + conf.MustSet(ctx, "selfservice.methods.passkey", nil) + conf.MustSet(ctx, "selfservice.methods.webauthn", nil) + conf.MustSet(ctx, "selfservice.methods.code", nil) + }) + + t.Run("account enumeration mitigation enabled", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true) + + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, nil) + }) + + check := func(t *testing.T, body string, isAPI bool) { + t.Logf("%s", body) + if !isAPI { + assert.Contains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginWebAuthn), "we do expect to see a webauthn trigger:\n%s", body) + assert.Contains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginPasskey), "we do expect to see a passkey trigger button:\n%s", body) + } + + assert.Equal(t, "hidden", gjson.Get(body, "ui.nodes.#(attributes.name==identifier).attributes.type").String(), "identifier is hidden to appear that we found an identity even though we did not") + + assert.NotContains(t, body, text.NewErrorValidationAccountNotFound().Text, "we do not expect to see an account not found error:\n%s", body) + + assert.Contains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginPassword), "we do expect to see a password trigger:\n%s", body) + + // We do expect to see the same social sign in buttons that were on the first page: + assert.Contains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginWith), "we do expect to see a oidc trigger:\n%s", body) + assert.Contains(t, body, "google", "we do expect to see a google trigger:\n%s", body) + } + + values := func(v url.Values) { + v.Set("identifier", "identifier") + v.Set("method", "identifier_first") + } + + t.Run("type=browser", func(t *testing.T) { + check(t, expectValidationError(t, false, false, false, values), false) + }) + + t.Run("type=SPA", func(t *testing.T) { + check(t, expectValidationError(t, false, false, true, values), false) + }) + + t.Run("type=api", func(t *testing.T) { + check(t, expectValidationError(t, true, false, false, values), true) + }) + }) + + t.Run("account enumeration mitigation disabled", func(t *testing.T) { + conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false) + t.Cleanup(func() { + conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, nil) + }) + + check := func(t *testing.T, body string) { + t.Logf("%s", body) + + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "ui.action").String(), publicTS.URL+login.RouteSubmitFlow, "%s", body) + assert.Contains(t, body, text.NewErrorValidationAccountNotFound().Text, "we do expect to see an error that the account does not exist: %s", body) + + assert.Equal(t, "text", gjson.Get(body, "ui.nodes.#(attributes.name==identifier).attributes.type").String(), "identifier is not hidden and we can see the input field as well") + + assert.NotContains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginPasskey), "we do not expect to see a passkey trigger button: %s", body) + assert.NotContains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginWebAuthn), "we do not expect to see a webauthn trigger: %s", body) + assert.NotContains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginPassword), "we do not expect to see a password trigger: %s", body) + + assert.NotContains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginWith), "we do not expect to see a oidc trigger: %s", body) + assert.NotContains(t, body, "google", "we do not expect to see a google trigger: %s", body) + } + + values := func(v url.Values) { + v.Set("identifier", "identifier") + v.Set("method", "identifier_first") + } + + t.Run("type=browser", func(t *testing.T) { + check(t, expectValidationError(t, false, false, false, values)) + }) + + t.Run("type=SPA", func(t *testing.T) { + check(t, expectValidationError(t, false, false, true, values)) + }) + + t.Run("type=api", func(t *testing.T) { + check(t, expectValidationError(t, true, false, false, values)) + }) + }) + }) + + t.Run("should pass with real request", func(t *testing.T) { + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + + firstValues := func(v url.Values) { + v.Set("identifier", identifier) + v.Set("method", "identifier_first") + } + + secondValues := func(v url.Values) { + v.Set("identifier", identifier) + v.Set("password", pwd) + v.Set("method", "password") + } + + t.Run("type=browser", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + + secondStep := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, firstValues, + true, false, http.StatusBadRequest, publicTS.URL+login.RouteSubmitFlow) + t.Logf("secondStep: %s", secondStep) + assert.Contains(t, secondStep, "current-password") + assert.Contains(t, secondStep, `"value":"password"`) + + body := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, secondValues, + false, false, http.StatusOK, redirTS.URL) + + assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body) + }) + + t.Run("type=spa", func(t *testing.T) { + hc := testhelpers.NewClientWithCookies(t) + + secondStep := testhelpers.SubmitLoginForm(t, false, hc, publicTS, firstValues, + true, false, http.StatusBadRequest, publicTS.URL+login.RouteSubmitFlow) + t.Logf("secondStep: %s", secondStep) + assert.Contains(t, secondStep, "current-password") + assert.Contains(t, secondStep, `"value":"password"`) + + body := testhelpers.SubmitLoginForm(t, false, hc, publicTS, secondValues, + true, false, http.StatusOK, publicTS.URL+login.RouteSubmitFlow) + + assert.Equal(t, identifier, gjson.Get(body, "session.identity.traits.subject").String(), "%s", body) + assert.Empty(t, gjson.Get(body, "session_token").String(), "%s", body) + assert.Empty(t, gjson.Get(body, "session.token").String(), "%s", body) + + // Was the session cookie set? + require.NotEmpty(t, hc.Jar.Cookies(urlx.ParseOrPanic(publicTS.URL)), "%+v", hc.Jar) + }) + + t.Run("type=api", func(t *testing.T) { + secondStep := testhelpers.SubmitLoginForm(t, true, nil, publicTS, firstValues, + false, false, http.StatusBadRequest, publicTS.URL+login.RouteSubmitFlow) + t.Logf("secondStep: %s", secondStep) + assert.Contains(t, secondStep, "current-password") + assert.Contains(t, secondStep, `"value":"password"`) + + body := testhelpers.SubmitLoginForm(t, true, nil, publicTS, secondValues, + false, false, http.StatusOK, publicTS.URL+login.RouteSubmitFlow) + + assert.Equal(t, identifier, gjson.Get(body, "session.identity.traits.subject").String(), "%s", body) + st := gjson.Get(body, "session_token").String() + assert.NotEmpty(t, st, "%s", body) + }) + }) +} + +func TestFormHydration(t *testing.T) { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceLoginFlowStyle, "identifier_first") + + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/default.schema.json") + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsType(node.IdentifierFirstGroup)) + require.NoError(t, err) + fh, ok := s.(login.FormHydrator) + require.True(t, ok) + + toSnapshot := func(t *testing.T, f *login.Flow) { + t.Helper() + // The CSRF token has a unique value that messes with the snapshot - ignore it. + f.UI.Nodes.ResetNodes("csrf_token") + snapshotx.SnapshotT(t, f.UI.Nodes) + } + newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) { + r := httptest.NewRequest("GET", "/self-service/login/browser", nil) + r = r.WithContext(ctx) + t.Helper() + f, err := login.NewFlow(conf, time.Minute, "csrf_token", r, flow.TypeBrowser) + require.NoError(t, err) + return r, f + } + + t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) { + r, f := newFlow(ctx, t) + f.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodFirstFactorRefresh", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodRefresh", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodSecondFactorRefresh(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) { + t.Run("case=no options", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=WithIdentifier", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=WithIdentityHint", func(t *testing.T) { + t.Run("case=account enumeration mitigation enabled", func(t *testing.T) { + ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true) + + id := identity.NewIdentity("default") + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=account enumeration mitigation disabled", func(t *testing.T) { + ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false) + + t.Run("case=identity has password", func(t *testing.T) { + id := identity.NewIdentity("default") + + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=identity does not have a password", func(t *testing.T) { + id := identity.NewIdentity("default") + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + }) + }) + }) + + t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f)) + toSnapshot(t, f) + }) +} diff --git a/selfservice/strategy/idfirst/strategy_test.go b/selfservice/strategy/idfirst/strategy_test.go new file mode 100644 index 000000000000..9a05358d25cf --- /dev/null +++ b/selfservice/strategy/idfirst/strategy_test.go @@ -0,0 +1,90 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package idfirst_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/x" + "github.com/ory/x/sqlxx" + + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/strategy/idfirst" + "github.com/ory/kratos/ui/node" + + "github.com/stretchr/testify/assert" + + "github.com/ory/kratos/identity" +) + +func TestCountActiveFirstFactorCredentials(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + s := idfirst.NewStrategy(reg) + cc := make(map[identity.CredentialsType]identity.Credentials) + + count, err := s.CountActiveFirstFactorCredentials(cc) + assert.NoError(t, err) + assert.Equal(t, 0, count) +} + +func TestCountActiveMultiFactorCredentials(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + s := idfirst.NewStrategy(reg) + cc := make(map[identity.CredentialsType]identity.Credentials) + + count, err := s.CountActiveMultiFactorCredentials(cc) + assert.NoError(t, err) + assert.Equal(t, 0, count) +} + +func TestCompletedAuthenticationMethod(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + s := idfirst.NewStrategy(reg) + ctx := context.Background() + + method := s.CompletedAuthenticationMethod(ctx) + assert.Equal(t, s.ID(), method.Method) + assert.Equal(t, identity.NoAuthenticatorAssuranceLevel, method.AAL) +} + +func TestNodeGroup(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + s := idfirst.NewStrategy(reg) + + group := s.NodeGroup() + assert.Equal(t, node.IdentifierFirstGroup, group) +} + +func createIdentity(ctx context.Context, reg *driver.RegistryDefault, t *testing.T, identifier, password string) *identity.Identity { + p, _ := reg.Hasher(ctx).Generate(context.Background(), []byte(password)) + iId := x.NewUUID() + id := &identity.Identity{ + ID: iId, + Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)), + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{identifier}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`), + }, + }, + VerifiableAddresses: []identity.VerifiableAddress{ + { + ID: x.NewUUID(), + Value: identifier, + Verified: false, + CreatedAt: time.Now(), + IdentityID: iId, + }, + }, + } + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), id)) + return id +} diff --git a/selfservice/strategy/idfirst/stub/default.schema.json b/selfservice/strategy/idfirst/stub/default.schema.json new file mode 100644 index 000000000000..8dc923266050 --- /dev/null +++ b/selfservice/strategy/idfirst/stub/default.schema.json @@ -0,0 +1,29 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + } + } + } + } +} diff --git a/selfservice/strategy/idfirst/types.go b/selfservice/strategy/idfirst/types.go new file mode 100644 index 000000000000..a8838043782a --- /dev/null +++ b/selfservice/strategy/idfirst/types.go @@ -0,0 +1,29 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package idfirst + +import "encoding/json" + +// Update Login Flow with Multi-Step Method +// +// swagger:model updateLoginFlowWithIdentifierFirstMethod +type updateLoginFlowWithIdentifierFirstMethod struct { + // Method should be set to "password" when logging in using the identifier and password strategy. + // + // required: true + Method string `json:"method"` + + // Sending the anti-csrf token is only required for browser login flows. + CSRFToken string `json:"csrf_token"` + + // Identifier is the email or username of the user trying to log in. + // + // required: true + Identifier string `json:"identifier"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` +} diff --git a/selfservice/strategy/link/.schema/recovery.schema.json b/selfservice/strategy/link/.schema/recovery.schema.json index e633741bc15b..cc4439c11351 100644 --- a/selfservice/strategy/link/.schema/recovery.schema.json +++ b/selfservice/strategy/link/.schema/recovery.schema.json @@ -19,6 +19,10 @@ }, "csrf_token": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/link/.schema/verification.schema.json b/selfservice/strategy/link/.schema/verification.schema.json index e633741bc15b..cc4439c11351 100644 --- a/selfservice/strategy/link/.schema/verification.schema.json +++ b/selfservice/strategy/link/.schema/verification.schema.json @@ -19,6 +19,10 @@ }, "csrf_token": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json index 3bb3cbbf3ef6..5ac9946936c8 100644 --- a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json +++ b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json @@ -43,8 +43,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json index 498575cfee1b..1a8d048fe37d 100644 --- a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json +++ b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json @@ -45,8 +45,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json index 3bb3cbbf3ef6..5ac9946936c8 100644 --- a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json +++ b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json @@ -43,8 +43,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } }, diff --git a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json index 498575cfee1b..1a8d048fe37d 100644 --- a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json +++ b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json @@ -45,8 +45,8 @@ "messages": [], "meta": { "label": { - "id": 1070005, - "text": "Submit", + "id": 1070009, + "text": "Continue", "type": "info" } } diff --git a/selfservice/strategy/link/sender.go b/selfservice/strategy/link/sender.go index 5ecd27bdcf10..41231a721b8b 100644 --- a/selfservice/strategy/link/sender.go +++ b/selfservice/strategy/link/sender.go @@ -73,11 +73,17 @@ func (s *Sender) SendRecoveryLink(ctx context.Context, f *recovery.Flow, via ide WithSensitiveField("email_address", address). WithField("was_notified", notifyUnknownRecipients). Info("Account recovery was requested for an unknown address.") + + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } if !notifyUnknownRecipients { // do nothing } else if err := s.send(ctx, string(via), email.NewRecoveryInvalid(s.r, &email.RecoveryInvalidModel{ - To: to, - RequestURL: f.GetRequestURL(), + To: to, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, })); err != nil { return err } @@ -125,11 +131,17 @@ func (s *Sender) SendVerificationLink(ctx context.Context, f *verification.Flow, WithSensitiveField("email_address", to). WithField("was_notified", notifyUnknownRecipients). Info("Address verification was requested for an unknown address.") + + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } if !notifyUnknownRecipients { // do nothing } else if err := s.send(ctx, string(via), email.NewVerificationInvalid(s.r, &email.VerificationInvalidModel{ - To: to, - RequestURL: f.GetRequestURL(), + To: to, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, })); err != nil { return err } @@ -170,15 +182,26 @@ func (s *Sender) SendRecoveryTokenTo(ctx context.Context, f *recovery.Flow, i *i return err } + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } + + recoveryUrl := urlx.CopyWithQuery( + urlx.AppendPaths(s.r.Config().SelfServiceLinkMethodBaseURL(ctx), recovery.RouteSubmitFlow), + url.Values{ + "token": {token.Token}, + "flow": {f.ID.String()}, + }). + String() + return s.send(ctx, string(address.Via), email.NewRecoveryValid(s.r, - &email.RecoveryValidModel{To: address.Value, RecoveryURL: urlx.CopyWithQuery( - urlx.AppendPaths(s.r.Config().SelfServiceLinkMethodBaseURL(ctx), recovery.RouteSubmitFlow), - url.Values{ - "token": {token.Token}, - "flow": {f.ID.String()}, - }).String(), - Identity: model, - RequestURL: f.GetRequestURL(), + &email.RecoveryValidModel{ + To: address.Value, + RecoveryURL: recoveryUrl, + Identity: model, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, })) } @@ -196,17 +219,25 @@ func (s *Sender) SendVerificationTokenTo(ctx context.Context, f *verification.Fl return err } + transientPayload, err := x.ParseRawMessageOrEmpty(f.GetTransientPayload()) + if err != nil { + return errors.WithStack(err) + } + + verificationUrl := urlx.CopyWithQuery( + urlx.AppendPaths(s.r.Config().SelfServiceLinkMethodBaseURL(ctx), verification.RouteSubmitFlow), + url.Values{ + "flow": {f.ID.String()}, + "token": {token.Token}, + }).String() + if err := s.send(ctx, string(address.Via), email.NewVerificationValid(s.r, &email.VerificationValidModel{ - To: address.Value, - VerificationURL: urlx.CopyWithQuery( - urlx.AppendPaths(s.r.Config().SelfServiceLinkMethodBaseURL(ctx), verification.RouteSubmitFlow), - url.Values{ - "flow": {f.ID.String()}, - "token": {token.Token}, - }).String(), - Identity: model, - RequestURL: f.GetRequestURL(), + To: address.Value, + VerificationURL: verificationUrl, + Identity: model, + RequestURL: f.GetRequestURL(), + TransientPayload: transientPayload, })); err != nil { return err } diff --git a/selfservice/strategy/link/strategy.go b/selfservice/strategy/link/strategy.go index 8188b7e9e873..cdf8356cc4b3 100644 --- a/selfservice/strategy/link/strategy.go +++ b/selfservice/strategy/link/strategy.go @@ -75,7 +75,7 @@ type ( VerificationTokenPersistenceProvider SenderProvider - schema.IdentityTraitsProvider + schema.IdentitySchemaProvider } Strategy struct { diff --git a/selfservice/strategy/link/strategy_recovery.go b/selfservice/strategy/link/strategy_recovery.go index 6297e780b646..8fff0986bf8d 100644 --- a/selfservice/strategy/link/strategy_recovery.go +++ b/selfservice/strategy/link/strategy_recovery.go @@ -4,6 +4,7 @@ package link import ( + "encoding/json" "net/http" "net/url" "time" @@ -55,7 +56,7 @@ func (s *Strategy) PopulateRecoveryMethod(r *http.Request, f *recovery.Flow) err // v0.5: form.Field{Name: "email", Type: "email", Required: true}, node.NewInputField("email", nil, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), ) - f.UI.GetNodes().Append(node.NewInputField("method", s.RecoveryStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSubmit())) + f.UI.GetNodes().Append(node.NewInputField("method", s.RecoveryStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue())) return nil } @@ -232,10 +233,15 @@ type updateRecoveryFlowWithLinkMethod struct { // // required: true Method recovery.RecoveryMethod `json:"method"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.Flow) (err error) { - ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.link.strategy.Recover") + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.link.Strategy.Recover") span.SetAttributes(attribute.String("selfservice_flows_recovery_use", s.d.Config().SelfServiceFlowRecoveryUse(ctx))) defer otelx.End(span, &err) @@ -244,6 +250,8 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F return s.HandleRecoveryError(w, r, nil, body, err) } + f.TransientPayload = body.TransientPayload + if len(body.Token) > 0 { if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.RecoveryStrategyID(), s.RecoveryStrategyID(), s.d); err != nil { return s.HandleRecoveryError(w, r, nil, body, err) @@ -275,9 +283,8 @@ func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.F } switch req.State { - case flow.StateChooseMethod: - fallthrough - case flow.StateEmailSent: + case flow.StateChooseMethod, + flow.StateEmailSent: return s.recoveryHandleFormSubmission(w, r, req) case flow.StatePassedChallenge: // was already handled, do not allow retry @@ -299,8 +306,9 @@ func (s *Strategy) recoveryIssueSession(w http.ResponseWriter, r *http.Request, return s.retryRecoveryFlowWithError(w, r, flow.TypeBrowser, err) } - sess, err := session.NewActiveSession(r, id, s.d.Config(), time.Now().UTC(), identity.CredentialsTypeRecoveryLink, identity.AuthenticatorAssuranceLevel1) - if err != nil { + sess := session.NewInactiveSession() + sess.CompletedLoginFor(identity.CredentialsTypeRecoveryLink, identity.AuthenticatorAssuranceLevel1) + if err := s.d.SessionManager().ActivateSession(r, sess, id, time.Now().UTC()); err != nil { return s.retryRecoveryFlowWithError(w, r, flow.TypeBrowser, err) } @@ -514,11 +522,12 @@ func (s *Strategy) HandleRecoveryError(w http.ResponseWriter, r *http.Request, r } type recoverySubmitPayload struct { - Method string `json:"method" form:"method"` - Token string `json:"token" form:"token"` - CSRFToken string `json:"csrf_token" form:"csrf_token"` - Flow string `json:"flow" form:"flow"` - Email string `json:"email" form:"email"` + Method string `json:"method" form:"method"` + Token string `json:"token" form:"token"` + CSRFToken string `json:"csrf_token" form:"csrf_token"` + Flow string `json:"flow" form:"flow"` + Email string `json:"email" form:"email"` + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) decodeRecovery(r *http.Request) (*recoverySubmitPayload, error) { diff --git a/selfservice/strategy/link/strategy_recovery_test.go b/selfservice/strategy/link/strategy_recovery_test.go index 41e57730a15b..5cc6c510d73e 100644 --- a/selfservice/strategy/link/strategy_recovery_test.go +++ b/selfservice/strategy/link/strategy_recovery_test.go @@ -5,7 +5,6 @@ package link_test import ( "context" - _ "embed" "encoding/json" "fmt" "net/http" @@ -15,44 +14,34 @@ import ( "testing" "time" - "github.com/ory/kratos/driver" - "github.com/ory/kratos/session" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" "github.com/davecgh/go-spew/spew" - "github.com/gofrs/uuid" - "github.com/pkg/errors" - - "github.com/ory/kratos/selfservice/flow" - "github.com/ory/kratos/selfservice/strategy/link" - "github.com/ory/kratos/ui/node" - - kratos "github.com/ory/kratos/internal/httpclient" - - "github.com/ory/kratos/corpx" - - "github.com/ory/x/ioutilx" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" - "github.com/ory/x/urlx" - - "github.com/ory/x/sqlxx" - - "github.com/ory/x/assertx" - - "github.com/ory/x/pointerx" - + "github.com/ory/kratos/corpx" + "github.com/ory/kratos/driver" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" + kratos "github.com/ory/kratos/internal/httpclient" "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/strategy/link" + "github.com/ory/kratos/session" "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/pointerx" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" ) func init() { @@ -112,7 +101,7 @@ func TestAdminStrategy(t *testing.T) { }) t.Run("description=should not be able to recover an account that does not exist", func(t *testing.T) { - _, _, err := adminSDK.IdentityApi.CreateRecoveryLinkForIdentity(context.Background()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ + _, _, err := adminSDK.IdentityAPI.CreateRecoveryLinkForIdentity(context.Background()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ IdentityId: x.NewUUID().String(), }).Execute() require.IsType(t, err, new(kratos.GenericOpenAPIError), "%T", err) @@ -125,9 +114,9 @@ func TestAdminStrategy(t *testing.T) { require.NoError(t, reg.IdentityManager().Create(context.Background(), &id, identity.ManagerAllowWriteProtectedTraits)) - rl, _, err := adminSDK.IdentityApi.CreateRecoveryLinkForIdentity(context.Background()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ + rl, _, err := adminSDK.IdentityAPI.CreateRecoveryLinkForIdentity(context.Background()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ IdentityId: id.ID.String(), - ExpiresIn: pointerx.String("100ms"), + ExpiresIn: pointerx.Ptr("100ms"), }).Execute() require.NoError(t, err) @@ -149,9 +138,9 @@ func TestAdminStrategy(t *testing.T) { require.NoError(t, reg.IdentityManager().Create(context.Background(), &id, identity.ManagerAllowWriteProtectedTraits)) - rl, _, err := adminSDK.IdentityApi.CreateRecoveryLinkForIdentity(context.Background()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ + rl, _, err := adminSDK.IdentityAPI.CreateRecoveryLinkForIdentity(context.Background()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ IdentityId: id.ID.String(), - ExpiresIn: pointerx.String("100ms"), + ExpiresIn: pointerx.Ptr("100ms"), }).Execute() require.NoError(t, err) @@ -179,7 +168,7 @@ func TestAdminStrategy(t *testing.T) { require.NoError(t, reg.IdentityManager().Create(context.Background(), &id, identity.ManagerAllowWriteProtectedTraits)) - rl, _, err := adminSDK.IdentityApi.CreateRecoveryLinkForIdentity(context.Background()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ + rl, _, err := adminSDK.IdentityAPI.CreateRecoveryLinkForIdentity(context.Background()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ IdentityId: id.ID.String(), }).Execute() require.NoError(t, err) @@ -209,7 +198,7 @@ func TestAdminStrategy(t *testing.T) { email := strings.ToLower(testhelpers.RandomEmail()) id := createIdentityToRecover(t, reg, email) - rl1, _, err := adminSDK.IdentityApi. + rl1, _, err := adminSDK.IdentityAPI. CreateRecoveryLinkForIdentity(context.Background()). CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ IdentityId: id.ID.String(), @@ -219,7 +208,7 @@ func TestAdminStrategy(t *testing.T) { checkLink(t, rl1, time.Now().Add(conf.SelfServiceFlowRecoveryRequestLifespan(ctx)+time.Second)) - rl2, _, err := adminSDK.IdentityApi. + rl2, _, err := adminSDK.IdentityAPI. CreateRecoveryLinkForIdentity(context.Background()). CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ IdentityId: id.ID.String(), @@ -264,6 +253,7 @@ func TestRecovery(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false) conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".link.enabled", true) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/default.schema.json") initViper(t, conf) _ = testhelpers.NewRecoveryUIFlowEchoServer(t, reg) @@ -382,18 +372,18 @@ func TestRecovery(t *testing.T) { v.Set("email", "some-email@example.org") v.Set("method", "link") - authClient := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg) + authClient := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg) if isAPI { req := httptest.NewRequest("GET", "/sessions/whoami", nil) - s, err := session.NewActiveSession(req, - &identity.Identity{ID: x.NewUUID(), State: identity.StateActive}, - testhelpers.NewSessionLifespanProvider(time.Hour), + req.WithContext(confighelpers.WithConfigValue(ctx, config.ViperKeySessionLifespan, time.Hour)) + s, err := testhelpers.NewActiveSession(req, reg, + &identity.Identity{ID: x.NewUUID(), State: identity.StateActive, NID: x.NewUUID()}, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1, ) require.NoError(t, err) - authClient = testhelpers.NewHTTPClientWithSessionCookieLocalhost(t, reg, s) + authClient = testhelpers.NewHTTPClientWithSessionCookieLocalhost(t, ctx, reg, s) } body, res := testhelpers.RecoveryMakeRequest(t, isAPI || isSPA, f, authClient, testhelpers.EncodeFormAsJSON(t, isAPI || isSPA, v)) @@ -688,7 +678,7 @@ func TestRecovery(t *testing.T) { v.Set("email", email) } - cl := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + cl := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) check(t, expectSuccess(t, nil, false, false, values), email, cl, func(_ *http.Client, req *http.Request) (*http.Response, error) { _, res := testhelpers.MockMakeAuthenticatedRequestWithClientAndID(t, reg, conf, publicRouter.Router, req, cl, id) return res, nil @@ -707,7 +697,7 @@ func TestRecovery(t *testing.T) { id := createIdentityToRecover(t, reg, email) req := httptest.NewRequest("GET", "/sessions/whoami", nil) - sess, err := session.NewActiveSession(req, id, conf, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + sess, err := testhelpers.NewActiveSession(req, reg, id, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) require.NoError(t, err) require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), sess)) @@ -751,7 +741,7 @@ func TestRecovery(t *testing.T) { assert.Equal(t, http.StatusOK, res.StatusCode) assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowRecoveryUI(ctx).String()+"?flow=") - rs, _, err := testhelpers.NewSDKCustomClient(public, c).FrontendApi.GetRecoveryFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute() + rs, _, err := testhelpers.NewSDKCustomClient(public, c).FrontendAPI.GetRecoveryFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute() require.NoError(t, err) require.Len(t, rs.Ui.Messages, 1) @@ -811,7 +801,7 @@ func TestRecovery(t *testing.T) { assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowRecoveryUI(ctx).String()) assert.NotContains(t, res.Request.URL.String(), gjson.Get(body, "id").String()) - rs, _, err := testhelpers.NewSDKCustomClient(public, c).FrontendApi.GetRecoveryFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute() + rs, _, err := testhelpers.NewSDKCustomClient(public, c).FrontendAPI.GetRecoveryFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute() require.NoError(t, err) require.Len(t, rs.Ui.Messages, 1) @@ -909,7 +899,7 @@ func TestDisabledEndpoint(t *testing.T) { require.NoError(t, reg.IdentityManager().Create(context.Background(), &id, identity.ManagerAllowWriteProtectedTraits)) - rl, _, err := adminSDK.IdentityApi.CreateRecoveryLinkForIdentity(context.Background()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ + rl, _, err := adminSDK.IdentityAPI.CreateRecoveryLinkForIdentity(context.Background()).CreateRecoveryLinkForIdentityBody(kratos.CreateRecoveryLinkForIdentityBody{ IdentityId: id.ID.String(), }).Execute() assert.Nil(t, rl) diff --git a/selfservice/strategy/link/strategy_verification.go b/selfservice/strategy/link/strategy_verification.go index 0db56233fa3f..2ab187c7b0d5 100644 --- a/selfservice/strategy/link/strategy_verification.go +++ b/selfservice/strategy/link/strategy_verification.go @@ -5,6 +5,7 @@ package link import ( "context" + "encoding/json" "net/http" "net/url" "time" @@ -31,10 +32,10 @@ func (s *Strategy) VerificationStrategyID() string { return string(verification.VerificationStrategyLink) } -func (s *Strategy) RegisterPublicVerificationRoutes(public *x.RouterPublic) { +func (s *Strategy) RegisterPublicVerificationRoutes(_ *x.RouterPublic) { } -func (s *Strategy) RegisterAdminVerificationRoutes(admin *x.RouterAdmin) { +func (s *Strategy) RegisterAdminVerificationRoutes(_ *x.RouterAdmin) { } func (s *Strategy) PopulateVerificationMethod(r *http.Request, f *verification.Flow) error { @@ -43,16 +44,17 @@ func (s *Strategy) PopulateVerificationMethod(r *http.Request, f *verification.F // v0.5: form.Field{Name: "email", Type: "email", Required: true} node.NewInputField("email", nil, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), ) - f.UI.GetNodes().Append(node.NewInputField("method", s.VerificationStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSubmit())) + f.UI.GetNodes().Append(node.NewInputField("method", s.VerificationStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue())) return nil } type verificationSubmitPayload struct { - Method string `json:"method" form:"method"` - Token string `json:"token" form:"token"` - CSRFToken string `json:"csrf_token" form:"csrf_token"` - Flow string `json:"flow" form:"flow"` - Email string `json:"email" form:"email"` + Method string `json:"method" form:"method"` + Token string `json:"token" form:"token"` + CSRFToken string `json:"csrf_token" form:"csrf_token"` + Flow string `json:"flow" form:"flow"` + Email string `json:"email" form:"email"` + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) decodeVerification(r *http.Request) (*verificationSubmitPayload, error) { @@ -77,12 +79,16 @@ func (s *Strategy) decodeVerification(r *http.Request) (*verificationSubmitPaylo } // handleVerificationError is a convenience function for handling all types of errors that may occur (e.g. validation error). -func (s *Strategy) handleVerificationError(w http.ResponseWriter, r *http.Request, f *verification.Flow, body *verificationSubmitPayload, err error) error { +func (s *Strategy) handleVerificationError(r *http.Request, f *verification.Flow, body *verificationSubmitPayload, err error) error { if f != nil { f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + email := "" + if body != nil { + email = body.Email + } f.UI.GetNodes().Upsert( // v0.5: form.Field{Name: "email", Type: "email", Required: true, Value: body.Body.Email} - node.NewInputField("email", body.Email, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), + node.NewInputField("email", email, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()), ) } @@ -115,65 +121,67 @@ type updateVerificationFlowWithLinkMethod struct { // // required: true Method verification.VerificationStrategy `json:"method"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) Verify(w http.ResponseWriter, r *http.Request, f *verification.Flow) (err error) { - ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.link.strategy.Verify") + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.link.Strategy.Verify") span.SetAttributes(attribute.String("selfservice_flows_verification_use", s.d.Config().SelfServiceFlowVerificationUse(ctx))) defer otelx.End(span, &err) - r = r.WithContext(ctx) body, err := s.decodeVerification(r) if err != nil { - return s.handleVerificationError(w, r, nil, body, err) + return s.handleVerificationError(r, nil, body, err) } + f.TransientPayload = body.TransientPayload if len(body.Token) > 0 { - if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.VerificationStrategyID(), s.VerificationStrategyID(), s.d); err != nil { - return s.handleVerificationError(w, r, nil, body, err) + if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.VerificationStrategyID(), s.VerificationStrategyID(), s.d); err != nil { + return s.handleVerificationError(r, nil, body, err) } - return s.verificationUseToken(w, r, body, f) + return s.verificationUseToken(ctx, w, r, body, f) } - if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.VerificationStrategyID(), body.Method, s.d); err != nil { - return s.handleVerificationError(w, r, f, body, err) + if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.VerificationStrategyID(), body.Method, s.d); err != nil { + return s.handleVerificationError(r, f, body, err) } if err := f.Valid(); err != nil { - return s.handleVerificationError(w, r, f, body, err) + return s.handleVerificationError(r, f, body, err) } switch f.State { - case flow.StateChooseMethod: - fallthrough - case flow.StateEmailSent: - // Do nothing (continue with execution after this switch statement) - return s.verificationHandleFormSubmission(w, r, f) + case flow.StateChooseMethod, flow.StateEmailSent: + return s.verificationHandleFormSubmission(ctx, r, f) case flow.StatePassedChallenge: - return s.retryVerificationFlowWithMessage(w, r, f.Type, text.NewErrorValidationVerificationRetrySuccess()) + return s.retryVerificationFlowWithMessage(ctx, w, r, f.Type, text.NewErrorValidationVerificationRetrySuccess()) default: - return s.retryVerificationFlowWithMessage(w, r, f.Type, text.NewErrorValidationVerificationStateFailure()) + return s.retryVerificationFlowWithMessage(ctx, w, r, f.Type, text.NewErrorValidationVerificationStateFailure()) } } -func (s *Strategy) verificationHandleFormSubmission(w http.ResponseWriter, r *http.Request, f *verification.Flow) error { +func (s *Strategy) verificationHandleFormSubmission(ctx context.Context, r *http.Request, f *verification.Flow) error { body, err := s.decodeVerification(r) if err != nil { - return s.handleVerificationError(w, r, f, body, err) + return s.handleVerificationError(r, f, body, err) } if len(body.Email) == 0 { - return s.handleVerificationError(w, r, f, body, schema.NewRequiredError("#/email", "email")) + return s.handleVerificationError(r, f, body, schema.NewRequiredError("#/email", "email")) } - if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, body.CSRFToken); err != nil { - return s.handleVerificationError(w, r, f, body, err) + if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, body.CSRFToken); err != nil { + return s.handleVerificationError(r, f, body, err) } - if err := s.d.LinkSender().SendVerificationLink(r.Context(), f, identity.VerifiableAddressTypeEmail, body.Email); err != nil { + if err := s.d.LinkSender().SendVerificationLink(ctx, f, identity.VerifiableAddressTypeEmail, body.Email); err != nil { if !errors.Is(err, ErrUnknownAddress) { - return s.handleVerificationError(w, r, f, body, err) + return s.handleVerificationError(r, f, body, err) } // Continue execution } @@ -188,33 +196,24 @@ func (s *Strategy) verificationHandleFormSubmission(w http.ResponseWriter, r *ht f.State = flow.StateEmailSent f.UI.Messages.Set(text.NewVerificationEmailSent()) if err := s.d.VerificationFlowPersister().UpdateVerificationFlow(r.Context(), f); err != nil { - return s.handleVerificationError(w, r, f, body, err) + return s.handleVerificationError(r, f, body, err) } return nil } -func (s *Strategy) verificationUseToken(w http.ResponseWriter, r *http.Request, body *verificationSubmitPayload, f *verification.Flow) error { - token, err := s.d.VerificationTokenPersister().UseVerificationToken(r.Context(), f.ID, body.Token) +func (s *Strategy) verificationUseToken(ctx context.Context, w http.ResponseWriter, r *http.Request, body *verificationSubmitPayload, f *verification.Flow) error { + token, err := s.d.VerificationTokenPersister().UseVerificationToken(ctx, f.ID, body.Token) if err != nil { if errors.Is(err, sqlcon.ErrNoRows) { - return s.retryVerificationFlowWithMessage(w, r, flow.TypeBrowser, text.NewErrorValidationVerificationTokenInvalidOrAlreadyUsed()) + return s.retryVerificationFlowWithMessage(ctx, w, r, flow.TypeBrowser, text.NewErrorValidationVerificationTokenInvalidOrAlreadyUsed()) } - return s.retryVerificationFlowWithError(w, r, flow.TypeBrowser, err) + return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err) } if err := token.Valid(); err != nil { - return s.retryVerificationFlowWithError(w, r, flow.TypeBrowser, err) - } - - i, err := s.d.IdentityPool().GetIdentity(r.Context(), token.VerifiableAddress.IdentityID, identity.ExpandDefault) - if err != nil { - return s.retryVerificationFlowWithError(w, r, flow.TypeBrowser, err) - } - - if err := s.d.VerificationExecutor().PostVerificationHook(w, r, f, i); err != nil { - return s.retryVerificationFlowWithError(w, r, flow.TypeBrowser, err) + return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err) } address := token.VerifiableAddress @@ -223,7 +222,12 @@ func (s *Strategy) verificationUseToken(w http.ResponseWriter, r *http.Request, address.VerifiedAt = &verifiedAt address.Status = identity.VerifiableAddressStatusCompleted if err := s.d.PrivilegedIdentityPool().UpdateVerifiableAddress(r.Context(), address); err != nil { - return s.retryVerificationFlowWithError(w, r, flow.TypeBrowser, err) + return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err) + } + + i, err := s.d.IdentityPool().GetIdentity(r.Context(), token.VerifiableAddress.IdentityID, identity.ExpandDefault) + if err != nil { + return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err) } returnTo := f.ContinueURL(r.Context(), s.d.Config()) @@ -248,61 +252,65 @@ func (s *Strategy) verificationUseToken(w http.ResponseWriter, r *http.Request, WithMetaLabel(text.NewInfoNodeLabelContinue())) if err := s.d.VerificationFlowPersister().UpdateVerificationFlow(r.Context(), f); err != nil { - return s.retryVerificationFlowWithError(w, r, flow.TypeBrowser, err) + return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err) + } + + if err := s.d.VerificationExecutor().PostVerificationHook(w, r, f, i); err != nil { + return s.retryVerificationFlowWithError(ctx, w, r, flow.TypeBrowser, err) } return nil } -func (s *Strategy) retryVerificationFlowWithMessage(w http.ResponseWriter, r *http.Request, ft flow.Type, message *text.Message) error { +func (s *Strategy) retryVerificationFlowWithMessage(ctx context.Context, w http.ResponseWriter, r *http.Request, ft flow.Type, message *text.Message) error { s.d.Logger().WithRequest(r).WithField("message", message).Debug("A verification flow is being retried because a validation error occurred.") f, err := verification.NewFlow(s.d.Config(), - s.d.Config().SelfServiceFlowVerificationRequestLifespan(r.Context()), s.d.CSRFHandler().RegenerateToken(w, r), r, s, ft) + s.d.Config().SelfServiceFlowVerificationRequestLifespan(ctx), s.d.CSRFHandler().RegenerateToken(w, r), r, s, ft) if err != nil { - return s.handleVerificationError(w, r, f, nil, err) + return s.handleVerificationError(r, f, nil, err) } f.UI.Messages.Add(message) - if err := s.d.VerificationFlowPersister().CreateVerificationFlow(r.Context(), f); err != nil { - return s.handleVerificationError(w, r, f, nil, err) + if err := s.d.VerificationFlowPersister().CreateVerificationFlow(ctx, f); err != nil { + return s.handleVerificationError(r, f, nil, err) } if ft == flow.TypeBrowser { - http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowVerificationUI(r.Context())).String(), http.StatusSeeOther) + http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther) } else { - http.Redirect(w, r, urlx.CopyWithQuery(urlx.AppendPaths(s.d.Config().SelfPublicURL(r.Context()), + http.Redirect(w, r, urlx.CopyWithQuery(urlx.AppendPaths(s.d.Config().SelfPublicURL(ctx), verification.RouteGetFlow), url.Values{"id": {f.ID.String()}}).String(), http.StatusSeeOther) } return errors.WithStack(flow.ErrCompletedByStrategy) } -func (s *Strategy) retryVerificationFlowWithError(w http.ResponseWriter, r *http.Request, ft flow.Type, verErr error) error { +func (s *Strategy) retryVerificationFlowWithError(ctx context.Context, w http.ResponseWriter, r *http.Request, ft flow.Type, verErr error) error { s.d.Logger().WithRequest(r).WithError(verErr).Debug("A verification flow is being retried because an error occurred.") f, err := verification.NewFlow(s.d.Config(), - s.d.Config().SelfServiceFlowVerificationRequestLifespan(r.Context()), s.d.CSRFHandler().RegenerateToken(w, r), r, s, ft) + s.d.Config().SelfServiceFlowVerificationRequestLifespan(ctx), s.d.CSRFHandler().RegenerateToken(w, r), r, s, ft) if err != nil { - return s.handleVerificationError(w, r, f, nil, err) + return s.handleVerificationError(r, f, nil, err) } if expired := new(flow.ExpiredError); errors.As(verErr, &expired) { - return s.retryVerificationFlowWithMessage(w, r, ft, text.NewErrorValidationVerificationFlowExpired(expired.ExpiredAt)) + return s.retryVerificationFlowWithMessage(ctx, w, r, ft, text.NewErrorValidationVerificationFlowExpired(expired.ExpiredAt)) } else { if err := f.UI.ParseError(node.LinkGroup, verErr); err != nil { return err } } - if err := s.d.VerificationFlowPersister().CreateVerificationFlow(r.Context(), f); err != nil { - return s.handleVerificationError(w, r, f, nil, err) + if err := s.d.VerificationFlowPersister().CreateVerificationFlow(ctx, f); err != nil { + return s.handleVerificationError(r, f, nil, err) } if ft == flow.TypeBrowser { - http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowVerificationUI(r.Context())).String(), http.StatusSeeOther) + http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowVerificationUI(ctx)).String(), http.StatusSeeOther) } else { - http.Redirect(w, r, urlx.CopyWithQuery(urlx.AppendPaths(s.d.Config().SelfPublicURL(r.Context()), + http.Redirect(w, r, urlx.CopyWithQuery(urlx.AppendPaths(s.d.Config().SelfPublicURL(ctx), verification.RouteGetFlow), url.Values{"id": {f.ID.String()}}).String(), http.StatusSeeOther) } diff --git a/selfservice/strategy/link/strategy_verification_test.go b/selfservice/strategy/link/strategy_verification_test.go index cab8fec60b99..b2fd7c0f405a 100644 --- a/selfservice/strategy/link/strategy_verification_test.go +++ b/selfservice/strategy/link/strategy_verification_test.go @@ -11,6 +11,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "sync" "testing" "time" @@ -210,7 +211,7 @@ func TestVerification(t *testing.T) { assert.Equal(t, http.StatusOK, res.StatusCode) assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowVerificationUI(ctx).String()+"?flow=") - sr, _, err := testhelpers.NewSDKCustomClient(public, c).FrontendApi.GetVerificationFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute() + sr, _, err := testhelpers.NewSDKCustomClient(public, c).FrontendAPI.GetVerificationFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute() require.NoError(t, err) require.Len(t, sr.Ui.Messages, 1) @@ -262,7 +263,7 @@ func TestVerification(t *testing.T) { assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowVerificationUI(ctx).String()) assert.NotContains(t, res.Request.URL.String(), gjson.Get(body, "id").String()) - sr, _, err := testhelpers.NewSDKCustomClient(public, c).FrontendApi.GetVerificationFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute() + sr, _, err := testhelpers.NewSDKCustomClient(public, c).FrontendAPI.GetVerificationFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute() require.NoError(t, err) require.Len(t, sr.Ui.Messages, 1) @@ -270,6 +271,12 @@ func TestVerification(t *testing.T) { }) t.Run("description=should verify an email address", func(t *testing.T) { + var wg sync.WaitGroup + testhelpers.NewVerifyAfterHookWebHookTarget(ctx, t, conf, func(t *testing.T, msg []byte) { + defer wg.Done() + assert.EqualValues(t, true, gjson.GetBytes(msg, "identity.verifiable_addresses.0.verified").Bool(), string(msg)) + assert.EqualValues(t, "completed", gjson.GetBytes(msg, "identity.verifiable_addresses.0.status").String(), string(msg)) + }) check := func(t *testing.T, actual string) { assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual) assert.EqualValues(t, verificationEmail, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual) @@ -310,15 +317,21 @@ func TestVerification(t *testing.T) { } t.Run("type=browser", func(t *testing.T) { + wg.Add(1) check(t, expectSuccess(t, nil, false, false, values)) + wg.Wait() }) t.Run("type=spa", func(t *testing.T) { + wg.Add(1) check(t, expectSuccess(t, nil, false, true, values)) + wg.Wait() }) t.Run("type=api", func(t *testing.T) { + wg.Add(1) check(t, expectSuccess(t, nil, true, false, values)) + wg.Wait() }) }) diff --git a/selfservice/strategy/link/test/persistence.go b/selfservice/strategy/link/test/persistence.go index af5738eaae31..c28250836775 100644 --- a/selfservice/strategy/link/test/persistence.go +++ b/selfservice/strategy/link/test/persistence.go @@ -8,6 +8,8 @@ import ( "testing" "time" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" "github.com/ory/kratos/selfservice/flow" @@ -28,15 +30,14 @@ import ( "github.com/ory/kratos/x" ) -func TestPersister(ctx context.Context, conf *config.Config, p interface { +func TestPersister(ctx context.Context, p interface { persistence.Persister }, ) func(t *testing.T) { return func(t *testing.T) { nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") - conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) + ctx := confighelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"}) t.Run("token=recovery", func(t *testing.T) { newRecoveryToken := func(t *testing.T, email string) (*link.RecoveryToken, *recovery.Flow) { diff --git a/selfservice/strategy/lookup/.schema/login.schema.json b/selfservice/strategy/lookup/.schema/login.schema.json index e2af475932d3..763757813b45 100644 --- a/selfservice/strategy/lookup/.schema/login.schema.json +++ b/selfservice/strategy/lookup/.schema/login.schema.json @@ -15,6 +15,10 @@ }, "lookup_secret": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/lookup/.schema/settings.schema.json b/selfservice/strategy/lookup/.schema/settings.schema.json index 729a9bb1f5e8..d20cdb77dd32 100644 --- a/selfservice/strategy/lookup/.schema/settings.schema.json +++ b/selfservice/strategy/lookup/.schema/settings.schema.json @@ -20,6 +20,10 @@ }, "lookup_secret_confirm": { "type": "boolean" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/lookup/login.go b/selfservice/strategy/lookup/login.go index a325ceefd279..3c11d4967895 100644 --- a/selfservice/strategy/lookup/login.go +++ b/selfservice/strategy/lookup/login.go @@ -8,6 +8,10 @@ import ( "net/http" "time" + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" "github.com/ory/x/sqlxx" @@ -99,8 +103,12 @@ type updateLoginFlowWithLookupSecretMethod struct { Code string `json:"lookup_secret"` } -func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, sess *session.Session) (i *identity.Identity, err error) { +func (s *Strategy) Login(_ http.ResponseWriter, r *http.Request, f *login.Flow, sess *session.Session) (i *identity.Identity, err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.lookup.strategy.Login") + defer otelx.End(span, &err) + if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel2); err != nil { + span.SetAttributes(attribute.String("not_responsible_reason", "requested AAL is not AAL2")) return nil, err } @@ -116,11 +124,11 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, s.handleLoginError(r, f, err) } - if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { return nil, s.handleLoginError(r, f, err) } - i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), s.ID(), sess.IdentityID.String()) + i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, s.ID(), sess.IdentityID.String()) if errors.Is(err, sqlcon.ErrNoRows) { return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewNoLookupDefined())) } else if err != nil { @@ -148,25 +156,30 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewErrorValidationLookupInvalid())) } - toUpdate, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), sess.IdentityID) + // We can't use a transaction here because HydrateIdentityAssociations (used by update) does not support transactions. + toUpdate, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, sess.IdentityID) if err != nil { - return nil, err + return nil, s.handleLoginError(r, f, err) } encoded, err := json.Marshal(&o) if err != nil { - return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to encoded updated lookup secrets.").WithDebug(err.Error()))) + return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to encode updated lookup secrets.").WithDebug(err.Error()))) } c.Config = encoded toUpdate.SetCredentials(s.ID(), *c) - if err := s.d.PrivilegedIdentityPool().UpdateIdentity(r.Context(), toUpdate); err != nil { + // We can't use a transaction here because HydrateIdentityAssociations (used by update) does not support transactions. + if err := s.d.IdentityManager().Update(ctx, toUpdate, + // We need to allow write protected traits because we are updating the lookup secrets. + identity.ManagerAllowWriteProtectedTraits, + ); err != nil { return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to update identity.").WithDebug(err.Error()))) } f.Active = s.ID() - if err = s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), f); err != nil { + if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow.").WithDebug(err.Error()))) } diff --git a/selfservice/strategy/lookup/login_test.go b/selfservice/strategy/lookup/login_test.go index c4896962c660..c746d744f059 100644 --- a/selfservice/strategy/lookup/login_test.go +++ b/selfservice/strategy/lookup/login_test.go @@ -14,6 +14,8 @@ import ( "testing" "time" + "github.com/ory/kratos/selfservice/flow" + "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -55,7 +57,7 @@ func TestCompleteLogin(t *testing.T) { t.Run("case=lookup payload is set when identity has lookup", func(t *testing.T) { id, _ := createIdentity(t, reg) - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{"0.attributes.value"}) }) @@ -63,7 +65,7 @@ func TestCompleteLogin(t *testing.T) { t.Run("case=lookup payload is not set when identity has no lookup", func(t *testing.T) { id := createIdentityWithoutLookup(t, reg) - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) assertx.EqualAsJSON(t, nil, f.Ui.Nodes) }) @@ -71,7 +73,7 @@ func TestCompleteLogin(t *testing.T) { t.Run("case=lookup payload is not set when identity has no lookup", func(t *testing.T) { id := createIdentityWithoutLookup(t, reg) - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) assertx.EqualAsJSON(t, nil, f.Ui.Nodes) }) @@ -86,7 +88,7 @@ func TestCompleteLogin(t *testing.T) { } doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) { - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) return doAPIFlowWithClient(t, v, id, apiClient, false) } @@ -99,7 +101,7 @@ func TestCompleteLogin(t *testing.T) { } doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) { - browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) return doBrowserFlowWithClient(t, spa, v, id, browserClient, false) } @@ -235,30 +237,35 @@ func TestCompleteLogin(t *testing.T) { } t.Run("type=api", func(t *testing.T) { - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) body, res := doAPIFlowWithClient(t, payload("key-0"), id, apiClient, false) check(t, false, body, res, "key-0", 2) // We can still use another key body, res = doAPIFlowWithClient(t, payload("key-2"), id, apiClient, true) check(t, false, body, res, "key-2", 3) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) t.Run("type=browser", func(t *testing.T) { - browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) body, res := doBrowserFlowWithClient(t, false, payload("key-3"), id, browserClient, false) check(t, true, body, res, "key-3", 2) // We can still use another key body, res = doBrowserFlowWithClient(t, false, payload("key-5"), id, browserClient, true) check(t, true, body, res, "key-5", 3) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) t.Run("type=spa", func(t *testing.T) { - browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) body, res := doBrowserFlowWithClient(t, true, payload("key-6"), id, browserClient, false) check(t, false, body, res, "key-6", 2) // We can still use another key body, res = doBrowserFlowWithClient(t, true, payload("key-8"), id, browserClient, true) check(t, false, body, res, "key-8", 3) + + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", body) }) }) diff --git a/selfservice/strategy/lookup/settings.go b/selfservice/strategy/lookup/settings.go index e1b67e91a715..8bb1226c7354 100644 --- a/selfservice/strategy/lookup/settings.go +++ b/selfservice/strategy/lookup/settings.go @@ -10,6 +10,10 @@ import ( "net/http" "time" + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/otelx" + "github.com/pquerna/otp" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -85,6 +89,11 @@ type updateSettingsFlowWithLookupMethod struct { // // swagger:ignore Flow string `json:"flow"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (p *updateSettingsFlowWithLookupMethod) GetFlowID() uuid.UUID { @@ -95,7 +104,9 @@ func (p *updateSettingsFlowWithLookupMethod) SetFlowID(rid uuid.UUID) { p.Flow = rid.String() } -func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (*settings.UpdateContext, error) { +func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (_ *settings.UpdateContext, err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.lookup.strategy.Settings") + defer otelx.End(span, &err) // fandom-start enabled, err := s.isEnabledForIdentity(r.Context(), ss.IdentityID) if err != nil { @@ -108,29 +119,30 @@ func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings. var p updateSettingsFlowWithLookupMethod ctxUpdate, err := settings.PrepareUpdate(s.d, w, r, f, ss, settings.ContinuityKey(s.SettingsStrategyID()), &p) if errors.Is(err, settings.ErrContinuePreviousAction) { - return ctxUpdate, s.continueSettingsFlow(w, r, ctxUpdate, &p) + return ctxUpdate, s.continueSettingsFlow(ctx, r, ctxUpdate, p) } else if err != nil { - return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, p, err) } if err := s.decodeSettingsFlow(r, &p); err != nil { - return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, p, err) } if p.RegenerateLookup || p.RevealLookup || p.ConfirmLookup || p.DisableLookup { // This method has only two submit buttons p.Method = s.SettingsStrategyID() - if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { - return nil, s.handleSettingsError(w, r, ctxUpdate, &p, err) + if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + return nil, s.handleSettingsError(w, r, ctxUpdate, p, err) } } else { + span.SetAttributes(attribute.String("not_responsible_reason", "neither reveal, regenerate, confirm, nor disable was set")) return nil, errors.WithStack(flow.ErrStrategyNotResponsible) } // This does not come from the payload! p.Flow = ctxUpdate.Flow.ID.String() - if err := s.continueSettingsFlow(w, r, ctxUpdate, &p); err != nil { - return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) + if err := s.continueSettingsFlow(ctx, r, ctxUpdate, p); err != nil { + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, p, err) } return ctxUpdate, nil @@ -148,20 +160,17 @@ func (s *Strategy) decodeSettingsFlow(r *http.Request, dest interface{}) error { ) } -func (s *Strategy) continueSettingsFlow( - w http.ResponseWriter, r *http.Request, - ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithLookupMethod, -) error { +func (s *Strategy) continueSettingsFlow(ctx context.Context, r *http.Request, ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithLookupMethod) error { if p.ConfirmLookup || p.RevealLookup || p.RegenerateLookup || p.DisableLookup { - if err := flow.MethodEnabledAndAllowed(r.Context(), flow.SettingsFlow, s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(ctx, flow.SettingsFlow, s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { return err } - if err := flow.EnsureCSRF(s.d, r, ctxUpdate.Flow.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + if err := flow.EnsureCSRF(s.d, r, ctxUpdate.Flow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { return err } - if ctxUpdate.Session.AuthenticatedAt.Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(r.Context())).Before(time.Now()) { + if ctxUpdate.Session.AuthenticatedAt.Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(ctx)).Before(time.Now()) { return errors.WithStack(settings.NewFlowNeedsReAuth()) } } else { @@ -169,16 +178,16 @@ func (s *Strategy) continueSettingsFlow( } if p.ConfirmLookup { - return s.continueSettingsFlowConfirm(w, r, ctxUpdate, p) + return s.continueSettingsFlowConfirm(ctx, ctxUpdate) } else if p.RevealLookup { - if err := s.continueSettingsFlowReveal(w, r, ctxUpdate, p); err != nil { + if err := s.continueSettingsFlowReveal(ctx, ctxUpdate); err != nil { return err } return flow.ErrStrategyAsksToReturnToUI } else if p.DisableLookup { - return s.continueSettingsFlowDisable(w, r, ctxUpdate, p) + return s.continueSettingsFlowDisable(ctx, ctxUpdate) } else if p.RegenerateLookup { - if err := s.continueSettingsFlowRegenerate(w, r, ctxUpdate, p); err != nil { + if err := s.continueSettingsFlowRegenerate(ctx, ctxUpdate); err != nil { return err } // regen @@ -188,8 +197,8 @@ func (s *Strategy) continueSettingsFlow( return errors.New("ended up in unexpected state") } -func (s *Strategy) continueSettingsFlowDisable(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithLookupMethod) error { - i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), ctxUpdate.Session.Identity.ID) +func (s *Strategy) continueSettingsFlowDisable(ctx context.Context, ctxUpdate *settings.UpdateContext) error { + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ctxUpdate.Session.Identity.ID) if err != nil { return err } @@ -210,7 +219,7 @@ func (s *Strategy) continueSettingsFlowDisable(w http.ResponseWriter, r *http.Re return err } - if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(r.Context(), ctxUpdate.Flow); err != nil { + if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(ctx, ctxUpdate.Flow); err != nil { return err } @@ -218,8 +227,8 @@ func (s *Strategy) continueSettingsFlowDisable(w http.ResponseWriter, r *http.Re return nil } -func (s *Strategy) continueSettingsFlowReveal(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithLookupMethod) error { - hasLookup, err := s.identityHasLookup(r.Context(), ctxUpdate.Session.IdentityID) +func (s *Strategy) continueSettingsFlowReveal(ctx context.Context, ctxUpdate *settings.UpdateContext) error { + hasLookup, err := s.identityHasLookup(ctx, ctxUpdate.Session.IdentityID) if err != nil { return err } @@ -228,7 +237,7 @@ func (s *Strategy) continueSettingsFlowReveal(w http.ResponseWriter, r *http.Req return errors.WithStack(herodot.ErrBadRequest.WithReasonf("Can not reveal lookup codes because you have none.")) } - _, cred, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), s.ID(), ctxUpdate.Session.IdentityID.String()) + _, cred, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, s.ID(), ctxUpdate.Session.IdentityID.String()) if err != nil { return err } @@ -251,14 +260,14 @@ func (s *Strategy) continueSettingsFlowReveal(w http.ResponseWriter, r *http.Req return err } - if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(r.Context(), ctxUpdate.Flow); err != nil { + if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(ctx, ctxUpdate.Flow); err != nil { return err } return nil } -func (s *Strategy) continueSettingsFlowRegenerate(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithLookupMethod) error { +func (s *Strategy) continueSettingsFlowRegenerate(ctx context.Context, ctxUpdate *settings.UpdateContext) error { codes := make([]identity.RecoveryCode, numCodes) for k := range codes { codes[k] = identity.RecoveryCode{Code: randx.MustString(8, randx.AlphaLowerNum)} @@ -277,14 +286,14 @@ func (s *Strategy) continueSettingsFlowRegenerate(w http.ResponseWriter, r *http return err } - if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(r.Context(), ctxUpdate.Flow); err != nil { + if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(ctx, ctxUpdate.Flow); err != nil { return err } return nil } -func (s *Strategy) continueSettingsFlowConfirm(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithLookupMethod) error { +func (s *Strategy) continueSettingsFlowConfirm(ctx context.Context, ctxUpdate *settings.UpdateContext) error { codes := gjson.GetBytes(ctxUpdate.Flow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeyRegenerated)).Array() if len(codes) != numCodes { return errors.WithStack(herodot.ErrBadRequest.WithReasonf("You must (re-)generate recovery backup codes before you can save them.")) @@ -300,7 +309,7 @@ func (s *Strategy) continueSettingsFlowConfirm(w http.ResponseWriter, r *http.Re return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode totp options to JSON: %s", err)) } - i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), ctxUpdate.Session.Identity.ID) + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ctxUpdate.Session.Identity.ID) if err != nil { return err } @@ -316,12 +325,12 @@ func (s *Strategy) continueSettingsFlowConfirm(w http.ResponseWriter, r *http.Re return err } - if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(r.Context(), ctxUpdate.Flow); err != nil { + if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(ctx, ctxUpdate.Flow); err != nil { return err } // Since we added the method, it also means that we have authenticated it - if err := s.d.SessionManager().SessionAddAuthenticationMethods(r.Context(), ctxUpdate.Session.ID, session.AuthenticationMethod{ + if err := s.d.SessionManager().SessionAddAuthenticationMethods(ctx, ctxUpdate.Session.ID, session.AuthenticationMethod{ Method: s.ID(), AAL: identity.AuthenticatorAssuranceLevel2, }); err != nil { @@ -338,7 +347,7 @@ func (s *Strategy) identityHasLookup(ctx context.Context, id uuid.UUID) (bool, e return false, err } - count, err := s.CountActiveMultiFactorCredentials(confidential.Credentials) + count, err := s.CountActiveMultiFactorCredentials(ctx, confidential.Credentials) if err != nil { return false, err } @@ -374,7 +383,7 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity return nil } -func (s *Strategy) handleSettingsError(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithLookupMethod, err error) error { +func (s *Strategy) handleSettingsError(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithLookupMethod, err error) error { // Do not pause flow if the flow type is an API flow as we can't save cookies in those flows. if e := new(settings.FlowNeedsReAuth); errors.As(err, &e) && ctxUpdate.Flow != nil && ctxUpdate.Flow.Type == flow.TypeBrowser { if err := s.d.ContinuityManager().Pause(r.Context(), w, r, settings.ContinuityKey(s.SettingsStrategyID()), settings.ContinuityOptions(p, ctxUpdate.GetSessionIdentity())...); err != nil { diff --git a/selfservice/strategy/lookup/settings_test.go b/selfservice/strategy/lookup/settings_test.go index fce2be4c0974..52b08786fd5a 100644 --- a/selfservice/strategy/lookup/settings_test.go +++ b/selfservice/strategy/lookup/settings_test.go @@ -111,7 +111,7 @@ func TestCompleteSettings(t *testing.T) { conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"}) doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) { - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) v(values) @@ -120,7 +120,7 @@ func TestCompleteSettings(t *testing.T) { } doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) { - browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS) values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) v(values) @@ -129,7 +129,7 @@ func TestCompleteSettings(t *testing.T) { t.Run("case=hide recovery codes behind reveal button and show disable button", func(t *testing.T) { id, _ := createIdentity(t, reg) - browserClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) t.Run("case=spa", func(t *testing.T) { f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, true, publicTS) @@ -142,7 +142,7 @@ func TestCompleteSettings(t *testing.T) { }) t.Run("case=api", func(t *testing.T) { - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{"0.attributes.value"}) }) @@ -150,7 +150,7 @@ func TestCompleteSettings(t *testing.T) { t.Run("case=button for regeneration is displayed when identity has no recovery codes yet", func(t *testing.T) { id := createIdentityWithoutLookup(t, reg) - browserClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) t.Run("case=spa", func(t *testing.T) { f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, true, publicTS) @@ -163,7 +163,7 @@ func TestCompleteSettings(t *testing.T) { }) t.Run("case=api", func(t *testing.T) { - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{"0.attributes.value"}) }) @@ -389,7 +389,7 @@ func TestCompleteSettings(t *testing.T) { t.Run("type=api", func(t *testing.T) { id, _ := createIdentity(t, reg) - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) @@ -398,7 +398,7 @@ func TestCompleteSettings(t *testing.T) { payloadConfirm(values) actual, res := testhelpers.SettingsMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values)) - assert.Equal(t, http.StatusOK, res.StatusCode) + require.Equal(t, http.StatusOK, res.StatusCode) assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) @@ -410,7 +410,7 @@ func TestCompleteSettings(t *testing.T) { runBrowser := func(t *testing.T, spa bool) { id, _ := createIdentity(t, reg) - browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS) values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) @@ -423,8 +423,11 @@ func TestCompleteSettings(t *testing.T) { if spa { assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", actual) } else { assert.Contains(t, res.Request.URL.String(), uiTS.URL) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) } assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) @@ -480,7 +483,7 @@ func TestCompleteSettings(t *testing.T) { t.Run("type=api", func(t *testing.T) { id, _ := createIdentity(t, reg) - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) @@ -498,7 +501,7 @@ func TestCompleteSettings(t *testing.T) { runBrowser := func(t *testing.T, spa bool) { id, _ := createIdentity(t, reg) - browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS) values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) @@ -508,8 +511,11 @@ func TestCompleteSettings(t *testing.T) { if spa { assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", actual) } else { assert.Contains(t, res.Request.URL.String(), uiTS.URL) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) } assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String())) diff --git a/selfservice/strategy/lookup/strategy.go b/selfservice/strategy/lookup/strategy.go index d37bbee22e58..938c8b4ed2ff 100644 --- a/selfservice/strategy/lookup/strategy.go +++ b/selfservice/strategy/lookup/strategy.go @@ -34,6 +34,8 @@ type lookupStrategyDependencies interface { x.WriterProvider x.CSRFTokenGeneratorProvider x.CSRFProvider + x.TransactionPersistenceProvider + x.TracingProvider config.Provider @@ -61,6 +63,7 @@ type lookupStrategyDependencies interface { identity.PrivilegedPoolProvider identity.ValidationProvider + identity.ManagementProvider session.HandlerProvider session.ManagementProvider @@ -78,11 +81,11 @@ func NewStrategy(d any) *Strategy { } } -func (s *Strategy) CountActiveFirstFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { +func (s *Strategy) CountActiveFirstFactorCredentials(_ context.Context, _ map[identity.CredentialsType]identity.Credentials) (count int, err error) { return 0, nil } -func (s *Strategy) CountActiveMultiFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { +func (s *Strategy) CountActiveMultiFactorCredentials(_ context.Context, cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { hasTotp := false for _, c := range cc { if c.Type == identity.CredentialsTypeTOTP { @@ -90,7 +93,7 @@ func (s *Strategy) CountActiveMultiFactorCredentials(cc map[identity.Credentials } if c.Type == s.ID() && len(c.Config) > 0 { var conf identity.CredentialsLookupConfig - if err = json.Unmarshal(c.Config, &conf); err != nil { + if err := json.Unmarshal(c.Config, &conf); err != nil { return 0, errors.WithStack(err) } @@ -117,7 +120,7 @@ func (s *Strategy) NodeGroup() node.UiNodeGroup { return node.LookupGroup } -func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context, _ session.AuthenticationMethods) session.AuthenticationMethod { +func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.AuthenticationMethod { return session.AuthenticationMethod{ Method: s.ID(), AAL: identity.AuthenticatorAssuranceLevel2, diff --git a/selfservice/strategy/lookup/strategy_test.go b/selfservice/strategy/lookup/strategy_test.go index df50aa711a68..1b16258f07d5 100644 --- a/selfservice/strategy/lookup/strategy_test.go +++ b/selfservice/strategy/lookup/strategy_test.go @@ -24,7 +24,7 @@ func TestCountActiveFirstFactorCredentials(t *testing.T) { strategy := lookup.NewStrategy(reg) t.Run("first factor", func(t *testing.T) { - actual, err := strategy.CountActiveFirstFactorCredentials(nil) + actual, err := strategy.CountActiveFirstFactorCredentials(nil, nil) require.NoError(t, err) assert.Equal(t, 0, actual) }) @@ -87,7 +87,7 @@ func TestCountActiveFirstFactorCredentials(t *testing.T) { } { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { conf.MustSet(ctx, fmt.Sprintf("%s.%s.config", config.ViperKeySelfServiceStrategyConfig, strategy.ID()), tc.config) - actual, err := strategy.CountActiveMultiFactorCredentials(tc.in) + actual, err := strategy.CountActiveMultiFactorCredentials(nil, tc.in) require.NoError(t, err) assert.Equal(t, tc.expected, actual) }) diff --git a/selfservice/strategy/oidc/.schema/settings.schema.json b/selfservice/strategy/oidc/.schema/settings.schema.json index 2eee443b9ca6..0c9dae1c6460 100644 --- a/selfservice/strategy/oidc/.schema/settings.schema.json +++ b/selfservice/strategy/oidc/.schema/settings.schema.json @@ -33,6 +33,10 @@ }, "additionalProperties": false } + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json new file mode 100644 index 000000000000..2939b127e281 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json @@ -0,0 +1,38 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "test-provider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010002, + "text": "Sign in with test-provider", + "type": "info", + "context": { + "provider": "test-provider", + "provider_id": "test-provider" + } + } + } + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json new file mode 100644 index 000000000000..2939b127e281 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json @@ -0,0 +1,38 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "test-provider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010002, + "text": "Sign in with test-provider", + "type": "info", + "context": { + "provider": "test-provider", + "provider_id": "test-provider" + } + } + } + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_oidc.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_oidc.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_oidc.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_oidc.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_oidc.json new file mode 100644 index 000000000000..a2c08bd8d1b9 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_oidc.json @@ -0,0 +1,25 @@ +[ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "test-provider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010002, + "text": "Sign in with test-provider", + "type": "info", + "context": { + "provider": "test-provider", + "provider_id": "test-provider" + } + } + } + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json new file mode 100644 index 000000000000..2939b127e281 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json @@ -0,0 +1,38 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "test-provider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010002, + "text": "Sign in with test-provider", + "type": "info", + "context": { + "provider": "test-provider", + "provider_id": "test-provider" + } + } + } + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh.json new file mode 100644 index 000000000000..9ce35531c24a --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh.json @@ -0,0 +1,37 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "test-provider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010002, + "text": "Sign in with test-provider", + "type": "info", + "context": { + "provider": "test-provider" + } + } + } + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json new file mode 100644 index 000000000000..364b8abc331c --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json @@ -0,0 +1,15 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=githuber.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=githuber.json index 5fb6dea7d7db..60a4f8eb5565 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=githuber.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=githuber.json @@ -155,10 +155,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=multiuser.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=multiuser.json index 86822e399e18..b9b9e01dc8d7 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=multiuser.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=multiuser.json @@ -155,10 +155,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=password.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=password.json index fcbf804b432a..d5ccdafec67f 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=password.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=password.json @@ -155,10 +155,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050002, - "text": "Link ory", + "text": "Link Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=fetch.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=fetch.json index 7d567598f996..b2fea59bee88 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=fetch.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=fetch.json @@ -155,10 +155,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=original.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=original.json index 5fb6dea7d7db..60a4f8eb5565 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=original.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=original.json @@ -155,10 +155,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=response.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=response.json index 31e953d9429f..e1cf44a6d068 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=response.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=response.json @@ -156,10 +156,10 @@ "meta": { "label": { "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info", "context": { - "provider": "ory" + "provider": "Ory" } } } diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection_even_if_user_does_not_have_oidc_credentials_yet.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection_even_if_user_does_not_have_oidc_credentials_yet.json index ad948dc9829b..a4a8f313f6e6 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection_even_if_user_does_not_have_oidc_credentials_yet.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection_even_if_user_does_not_have_oidc_credentials_yet.json @@ -155,10 +155,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050002, - "text": "Link ory", + "text": "Link Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_a_connection_which_already_exists.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_a_connection_which_already_exists.json index 340d6133d6ac..cc0ec90d049f 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_a_connection_which_already_exists.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_a_connection_which_already_exists.json @@ -156,10 +156,10 @@ "meta": { "label": { "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info", "context": { - "provider": "ory" + "provider": "Ory" } } } diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=fetch.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=fetch.json index 5fb6dea7d7db..60a4f8eb5565 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=fetch.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=fetch.json @@ -155,10 +155,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=json.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=json.json index 340d6133d6ac..cc0ec90d049f 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=json.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=json.json @@ -156,10 +156,10 @@ "meta": { "label": { "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info", "context": { - "provider": "ory" + "provider": "Ory" } } } diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=fetch.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=fetch.json index 5fb6dea7d7db..60a4f8eb5565 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=fetch.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=fetch.json @@ -155,10 +155,10 @@ "meta": { "label": { "context": { - "provider": "ory" + "provider": "Ory" }, "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info" } }, diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=json.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=json.json index 340d6133d6ac..cc0ec90d049f 100644 --- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=json.json +++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=json.json @@ -156,10 +156,10 @@ "meta": { "label": { "id": 1050003, - "text": "Unlink ory", + "text": "Unlink Ory", "type": "info", "context": { - "provider": "ory" + "provider": "Ory" } } } diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_login.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_login.json new file mode 100644 index 000000000000..cfcda57ec4e1 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_login.json @@ -0,0 +1,254 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "claimsViaUserInfo", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with claimsViaUserInfo", + "type": "info", + "context": { + "provider": "claimsViaUserInfo" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "invalid-issuer", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with invalid-issuer", + "type": "info", + "context": { + "provider": "invalid-issuer" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "valid2", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with valid2", + "type": "info", + "context": { + "provider": "valid2" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-oidc-strategy-lh-false@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-oidc-strategy-lh-false@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-oidc-strategy-lh-false@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": [], + "available_providers": [], + "duplicateIdentifier": "email-exist-with-oidc-strategy-lh-false@ory.sh", + "duplicate_identifier": "email-exist-with-oidc-strategy-lh-false@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration.json new file mode 100644 index 000000000000..cfcda57ec4e1 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration.json @@ -0,0 +1,254 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "claimsViaUserInfo", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with claimsViaUserInfo", + "type": "info", + "context": { + "provider": "claimsViaUserInfo" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "invalid-issuer", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with invalid-issuer", + "type": "info", + "context": { + "provider": "invalid-issuer" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "valid2", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with valid2", + "type": "info", + "context": { + "provider": "valid2" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-oidc-strategy-lh-false@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-oidc-strategy-lh-false@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-oidc-strategy-lh-false@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": [], + "available_providers": [], + "duplicateIdentifier": "email-exist-with-oidc-strategy-lh-false@ory.sh", + "duplicate_identifier": "email-exist-with-oidc-strategy-lh-false@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration_id_first_strategy_enabled.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration_id_first_strategy_enabled.json new file mode 100644 index 000000000000..cfcda57ec4e1 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration_id_first_strategy_enabled.json @@ -0,0 +1,254 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "claimsViaUserInfo", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with claimsViaUserInfo", + "type": "info", + "context": { + "provider": "claimsViaUserInfo" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "invalid-issuer", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with invalid-issuer", + "type": "info", + "context": { + "provider": "invalid-issuer" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "valid2", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with valid2", + "type": "info", + "context": { + "provider": "valid2" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-oidc-strategy-lh-false@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-oidc-strategy-lh-false@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-oidc-strategy-lh-false@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": [], + "available_providers": [], + "duplicateIdentifier": "email-exist-with-oidc-strategy-lh-false@ory.sh", + "duplicate_identifier": "email-exist-with-oidc-strategy-lh-false@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_login.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_login.json new file mode 100644 index 000000000000..5fbb69e1fcc6 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_login.json @@ -0,0 +1,254 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "claimsViaUserInfo", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with claimsViaUserInfo", + "type": "info", + "context": { + "provider": "claimsViaUserInfo" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "invalid-issuer", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with invalid-issuer", + "type": "info", + "context": { + "provider": "invalid-issuer" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "valid2", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with valid2", + "type": "info", + "context": { + "provider": "valid2" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-password-strategy-lh-false@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-password-strategy-lh-false@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy-lh-false@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": [], + "available_providers": [], + "duplicateIdentifier": "email-exist-with-password-strategy-lh-false@ory.sh", + "duplicate_identifier": "email-exist-with-password-strategy-lh-false@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration.json new file mode 100644 index 000000000000..5fbb69e1fcc6 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration.json @@ -0,0 +1,254 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "claimsViaUserInfo", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with claimsViaUserInfo", + "type": "info", + "context": { + "provider": "claimsViaUserInfo" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "invalid-issuer", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with invalid-issuer", + "type": "info", + "context": { + "provider": "invalid-issuer" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "valid2", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with valid2", + "type": "info", + "context": { + "provider": "valid2" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-password-strategy-lh-false@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-password-strategy-lh-false@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy-lh-false@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": [], + "available_providers": [], + "duplicateIdentifier": "email-exist-with-password-strategy-lh-false@ory.sh", + "duplicate_identifier": "email-exist-with-password-strategy-lh-false@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json new file mode 100644 index 000000000000..5fbb69e1fcc6 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json @@ -0,0 +1,254 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "claimsViaUserInfo", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with claimsViaUserInfo", + "type": "info", + "context": { + "provider": "claimsViaUserInfo" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "invalid-issuer", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with invalid-issuer", + "type": "info", + "context": { + "provider": "invalid-issuer" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "valid2", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with valid2", + "type": "info", + "context": { + "provider": "valid2" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-password-strategy-lh-false@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-password-strategy-lh-false@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy-lh-false@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": [], + "available_providers": [], + "duplicateIdentifier": "email-exist-with-password-strategy-lh-false@ory.sh", + "duplicate_identifier": "email-exist-with-password-strategy-lh-false@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_login.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_login.json new file mode 100644 index 000000000000..77bef5d097ae --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_login.json @@ -0,0 +1,83 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-oidc-strategy-lh-true@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-oidc-strategy-lh-true@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-oidc-strategy-lh-true@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": ["oidc"], + "available_providers": ["secondProvider"], + "duplicateIdentifier": "email-exist-with-oidc-strategy-lh-true@ory.sh", + "duplicate_identifier": "email-exist-with-oidc-strategy-lh-true@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration.json new file mode 100644 index 000000000000..77bef5d097ae --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration.json @@ -0,0 +1,83 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-oidc-strategy-lh-true@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-oidc-strategy-lh-true@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-oidc-strategy-lh-true@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": ["oidc"], + "available_providers": ["secondProvider"], + "duplicateIdentifier": "email-exist-with-oidc-strategy-lh-true@ory.sh", + "duplicate_identifier": "email-exist-with-oidc-strategy-lh-true@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration_id_first_strategy_enabled.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration_id_first_strategy_enabled.json new file mode 100644 index 000000000000..77bef5d097ae --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration_id_first_strategy_enabled.json @@ -0,0 +1,83 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-oidc-strategy-lh-true@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-oidc-strategy-lh-true@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-oidc-strategy-lh-true@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": ["oidc"], + "available_providers": ["secondProvider"], + "duplicateIdentifier": "email-exist-with-oidc-strategy-lh-true@ory.sh", + "duplicate_identifier": "email-exist-with-oidc-strategy-lh-true@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_login.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_login.json new file mode 100644 index 000000000000..93317c6e479a --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_login.json @@ -0,0 +1,100 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-password-strategy-lh-true@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-password-strategy-lh-true@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy-lh-true@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": ["password"], + "available_providers": [], + "duplicateIdentifier": "email-exist-with-password-strategy-lh-true@ory.sh", + "duplicate_identifier": "email-exist-with-password-strategy-lh-true@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration.json new file mode 100644 index 000000000000..93317c6e479a --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration.json @@ -0,0 +1,100 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-password-strategy-lh-true@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-password-strategy-lh-true@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy-lh-true@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": ["password"], + "available_providers": [], + "duplicateIdentifier": "email-exist-with-password-strategy-lh-true@ory.sh", + "duplicate_identifier": "email-exist-with-password-strategy-lh-true@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json new file mode 100644 index 000000000000..93317c6e479a --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json @@ -0,0 +1,100 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-password-strategy-lh-true@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-password-strategy-lh-true@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy-lh-true@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": ["password"], + "available_providers": [], + "duplicateIdentifier": "email-exist-with-password-strategy-lh-true@ory.sh", + "duplicate_identifier": "email-exist-with-password-strategy-lh-true@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateLoginMethod.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateLoginMethod.json deleted file mode 100644 index bacf802cd191..000000000000 --- a/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateLoginMethod.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "method": "POST", - "nodes": [ - { - "type": "input", - "group": "default", - "attributes": { - "name": "csrf_token", - "type": "hidden", - "required": true, - "disabled": false, - "node_type": "input" - }, - "messages": [], - "meta": {} - }, - { - "type": "input", - "group": "oidc", - "attributes": { - "name": "provider", - "type": "submit", - "value": "valid", - "disabled": false, - "node_type": "input" - }, - "messages": [], - "meta": { - "label": { - "id": 1010002, - "text": "Sign in with valid", - "type": "info", - "context": { - "provider": "valid" - } - } - } - }, - { - "type": "input", - "group": "oidc", - "attributes": { - "name": "provider", - "type": "submit", - "value": "secondProvider", - "disabled": false, - "node_type": "input" - }, - "messages": [], - "meta": { - "label": { - "id": 1010002, - "text": "Sign in with secondProvider", - "type": "info", - "context": { - "provider": "secondProvider" - } - } - } - }, - { - "type": "input", - "group": "oidc", - "attributes": { - "name": "provider", - "type": "submit", - "value": "claimsViaUserInfo", - "disabled": false, - "node_type": "input" - }, - "messages": [], - "meta": { - "label": { - "id": 1010002, - "text": "Sign in with claimsViaUserInfo", - "type": "info", - "context": { - "provider": "claimsViaUserInfo" - } - } - } - }, - { - "type": "input", - "group": "oidc", - "attributes": { - "name": "provider", - "type": "submit", - "value": "invalid-issuer", - "disabled": false, - "node_type": "input" - }, - "messages": [], - "meta": { - "label": { - "id": 1010002, - "text": "Sign in with invalid-issuer", - "type": "info", - "context": { - "provider": "invalid-issuer" - } - } - } - } - ] -} diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateSignUpMethod.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateSignUpMethod.json index 6b0a9ba98cf8..2177c514d3fd 100644 --- a/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateSignUpMethod.json +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateSignUpMethod.json @@ -31,7 +31,31 @@ "text": "Sign up with valid", "type": "info", "context": { - "provider": "valid" + "provider": "valid", + "provider_id": "valid" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "valid2", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with valid2", + "type": "info", + "context": { + "provider": "valid2", + "provider_id": "valid2" } } } @@ -53,7 +77,8 @@ "text": "Sign up with secondProvider", "type": "info", "context": { - "provider": "secondProvider" + "provider": "secondProvider", + "provider_id": "secondProvider" } } } @@ -75,7 +100,77 @@ "text": "Sign up with claimsViaUserInfo", "type": "info", "context": { - "provider": "claimsViaUserInfo" + "provider": "claimsViaUserInfo", + "provider_id": "claimsViaUserInfo" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE", + "provider_id": "neverPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE", + "provider_id": "autoPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE", + "provider_id": "forcePKCE" } } } @@ -97,7 +192,8 @@ "text": "Sign up with invalid-issuer", "type": "info", "context": { - "provider": "invalid-issuer" + "provider": "invalid-issuer", + "provider_id": "invalid-issuer" } } } diff --git a/selfservice/strategy/oidc/error.go b/selfservice/strategy/oidc/error.go index d0d4b14ab1fc..8fe984655a9a 100644 --- a/selfservice/strategy/oidc/error.go +++ b/selfservice/strategy/oidc/error.go @@ -28,7 +28,7 @@ func logUpstreamError(l *logrusx.Logger, resp *http.Response) error { return nil } - body, err := io.ReadAll(resp.Body) + body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) if err != nil { l = l.WithError(err) } diff --git a/selfservice/strategy/oidc/form.go b/selfservice/strategy/oidc/form.go index 69c86012fbf2..ff3f8cab4846 100644 --- a/selfservice/strategy/oidc/form.go +++ b/selfservice/strategy/oidc/form.go @@ -7,7 +7,7 @@ import ( "bytes" "encoding/json" - "github.com/imdario/mergo" + "dario.cat/mergo" "github.com/ory/kratos/identity" ) diff --git a/selfservice/strategy/oidc/nodes.go b/selfservice/strategy/oidc/nodes.go index e60dd6324d1a..3dc725e0d967 100644 --- a/selfservice/strategy/oidc/nodes.go +++ b/selfservice/strategy/oidc/nodes.go @@ -8,10 +8,10 @@ import ( "github.com/ory/kratos/ui/node" ) -func NewLinkNode(provider string) *node.Node { - return node.NewInputField("link", provider, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateLinkOIDC(provider)) +func NewLinkNode(providerID, providerLabel string) *node.Node { + return node.NewInputField("link", providerID, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateLinkOIDC(providerLabel)) } -func NewUnlinkNode(provider string) *node.Node { - return node.NewInputField("unlink", provider, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateUnlinkOIDC(provider)) +func NewUnlinkNode(providerID, providerLabel string) *node.Node { + return node.NewInputField("unlink", providerID, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateUnlinkOIDC(providerLabel)) } diff --git a/selfservice/strategy/oidc/pkce.go b/selfservice/strategy/oidc/pkce.go new file mode 100644 index 000000000000..2b397c8702b7 --- /dev/null +++ b/selfservice/strategy/oidc/pkce.go @@ -0,0 +1,79 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "slices" + + gooidc "github.com/coreos/go-oidc/v3/oidc" + "github.com/pkg/errors" + "golang.org/x/oauth2" + + oidcv1 "github.com/ory/kratos/gen/oidc/v1" + "github.com/ory/kratos/x" +) + +type pkceDependencies interface { + x.LoggingProvider + x.HTTPClientProvider +} + +func PKCEChallenge(s *oidcv1.State) []oauth2.AuthCodeOption { + if s.GetPkceVerifier() == "" { + return nil + } + return []oauth2.AuthCodeOption{oauth2.S256ChallengeOption(s.GetPkceVerifier())} +} + +func PKCEVerifier(s *oidcv1.State) []oauth2.AuthCodeOption { + if s.GetPkceVerifier() == "" { + return nil + } + return []oauth2.AuthCodeOption{oauth2.VerifierOption(s.GetPkceVerifier())} +} + +func maybePKCE(ctx context.Context, d pkceDependencies, _p Provider) (verifier string) { + if _p.Config().PKCE == "never" { + return "" + } + + p, ok := _p.(OAuth2Provider) + if !ok { + return "" + } + + if p.Config().PKCE != "force" { + // autodiscover PKCE support + pkceSupported, err := discoverPKCE(ctx, d, p) + if err != nil { + d.Logger().WithError(err).Warnf("Failed to autodiscover PKCE support for provider %q. Continuing without PKCE.", p.Config().ID) + return "" + } + if !pkceSupported { + d.Logger().Infof("Provider %q does not advertise support for PKCE. Continuing without PKCE.", p.Config().ID) + return "" + } + } + return oauth2.GenerateVerifier() +} + +func discoverPKCE(ctx context.Context, d pkceDependencies, p OAuth2Provider) (pkceSupported bool, err error) { + if p.Config().IssuerURL == "" { + return false, errors.New("Issuer URL must be set to autodiscover PKCE support") + } + + ctx = gooidc.ClientContext(ctx, d.HTTPClient(ctx).HTTPClient) + gp, err := gooidc.NewProvider(ctx, p.Config().IssuerURL) + if err != nil { + return false, errors.Wrap(err, "failed to initialize provider") + } + var claims struct { + CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"` + } + if err := gp.Claims(&claims); err != nil { + return false, errors.Wrap(err, "failed to deserialize provider claims") + } + return slices.Contains(claims.CodeChallengeMethodsSupported, "S256"), nil +} diff --git a/selfservice/strategy/oidc/pkce_test.go b/selfservice/strategy/oidc/pkce_test.go new file mode 100644 index 000000000000..24ef31c5d251 --- /dev/null +++ b/selfservice/strategy/oidc/pkce_test.go @@ -0,0 +1,89 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/x" +) + +func TestPKCESupport(t *testing.T) { + supported := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, `{"issuer": "http://%s", "code_challenge_methods_supported":["S256"]}`, r.Host) + })) + t.Cleanup(supported.Close) + notSupported := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, `{"issuer": "http://%s", "code_challenge_methods_supported": ["plain"]}`, r.Host) + })) + t.Cleanup(notSupported.Close) + + conf, reg := internal.NewFastRegistryWithMocks(t) + _ = conf + strat := oidc.NewStrategy(reg) + + for _, tc := range []struct { + c *oidc.Configuration + pkce bool + }{ + {c: &oidc.Configuration{IssuerURL: supported.URL, PKCE: "force"}, pkce: true}, + {c: &oidc.Configuration{IssuerURL: supported.URL, PKCE: "never"}, pkce: false}, + {c: &oidc.Configuration{IssuerURL: supported.URL, PKCE: "auto"}, pkce: true}, + {c: &oidc.Configuration{IssuerURL: supported.URL, PKCE: ""}, pkce: true}, // same as auto + + {c: &oidc.Configuration{IssuerURL: notSupported.URL, PKCE: "force"}, pkce: true}, + {c: &oidc.Configuration{IssuerURL: notSupported.URL, PKCE: "never"}, pkce: false}, + {c: &oidc.Configuration{IssuerURL: notSupported.URL, PKCE: "auto"}, pkce: false}, + {c: &oidc.Configuration{IssuerURL: notSupported.URL, PKCE: ""}, pkce: false}, // same as auto + + {c: &oidc.Configuration{IssuerURL: "", PKCE: "force"}, pkce: true}, + {c: &oidc.Configuration{IssuerURL: "", PKCE: "never"}, pkce: false}, + {c: &oidc.Configuration{IssuerURL: "", PKCE: "auto"}, pkce: false}, + {c: &oidc.Configuration{IssuerURL: "", PKCE: ""}, pkce: false}, // same as auto + + } { + provider := oidc.NewProviderGenericOIDC(tc.c, reg) + + stateParam, pkce, err := strat.GenerateState(context.Background(), provider, x.NewUUID()) + require.NoError(t, err) + require.NotEmpty(t, stateParam) + + state, err := oidc.DecryptState(context.Background(), reg.Cipher(context.Background()), stateParam) + require.NoError(t, err) + + if tc.pkce { + require.NotEmpty(t, pkce) + require.NotEmpty(t, oidc.PKCEVerifier(state)) + } else { + require.Empty(t, pkce) + require.Empty(t, oidc.PKCEVerifier(state)) + } + } + + t.Run("OAuth1", func(t *testing.T) { + for _, provider := range []oidc.Provider{ + oidc.NewProviderX(&oidc.Configuration{IssuerURL: supported.URL, PKCE: "force"}, reg), + oidc.NewProviderX(&oidc.Configuration{IssuerURL: supported.URL, PKCE: "never"}, reg), + oidc.NewProviderX(&oidc.Configuration{IssuerURL: supported.URL, PKCE: "auto"}, reg), + } { + stateParam, pkce, err := strat.GenerateState(context.Background(), provider, x.NewUUID()) + require.NoError(t, err) + require.NotEmpty(t, stateParam) + assert.Empty(t, pkce) + + state, err := oidc.DecryptState(context.Background(), reg.Cipher(context.Background()), stateParam) + require.NoError(t, err) + assert.Empty(t, oidc.PKCEVerifier(state)) + } + }) +} diff --git a/selfservice/strategy/oidc/provider.go b/selfservice/strategy/oidc/provider.go index 4fe9a028c11e..30ea305a22ed 100644 --- a/selfservice/strategy/oidc/provider.go +++ b/selfservice/strategy/oidc/provider.go @@ -5,8 +5,12 @@ package oidc import ( "context" + "encoding/json" + "net/http" "net/url" + "strings" + "github.com/dghubble/oauth1" "github.com/pkg/errors" "github.com/ory/herodot" @@ -18,12 +22,24 @@ import ( type Provider interface { Config() *Configuration +} + +type OAuth2Provider interface { + Provider + AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption OAuth2(ctx context.Context) (*oauth2.Config, error) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) - AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption } -type TokenExchanger interface { +type OAuth1Provider interface { + Provider + OAuth1(ctx context.Context) *oauth1.Config + AuthURL(ctx context.Context, state string) (string, error) + Claims(ctx context.Context, token *oauth1.Token) (*Claims, error) + ExchangeToken(ctx context.Context, req *http.Request) (*oauth1.Token, error) +} + +type OAuth2TokenExchanger interface { Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) } @@ -54,7 +70,7 @@ type Claims struct { Gender string `json:"gender,omitempty"` Birthdate string `json:"birthdate,omitempty"` Zoneinfo string `json:"zoneinfo,omitempty"` - Locale string `json:"locale,omitempty"` + Locale Locale `json:"locale,omitempty"` PhoneNumber string `json:"phone_number,omitempty"` PhoneNumberVerified bool `json:"phone_number_verified,omitempty"` UpdatedAt int64 `json:"updated_at,omitempty"` @@ -65,6 +81,29 @@ type Claims struct { RawClaims map[string]interface{} `json:"raw_claims,omitempty"` } +type Locale string + +func (l *Locale) UnmarshalJSON(data []byte) error { + var linkedInLocale struct { + Language string `json:"language"` + Country string `json:"country"` + } + if err := json.Unmarshal(data, &linkedInLocale); err == nil { + switch { + case linkedInLocale.Language == "": + *l = Locale(linkedInLocale.Country) + case linkedInLocale.Country == "": + *l = Locale(linkedInLocale.Language) + default: + *l = Locale(strings.Join([]string{linkedInLocale.Language, linkedInLocale.Country}, "-")) + } + + return nil + } + + return json.Unmarshal(data, (*string)(l)) +} + // Validate checks if the claims are valid. func (c *Claims) Validate() error { if c.Subject == "" { @@ -87,11 +126,11 @@ func (c *Claims) Validate() error { // - `hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`. // - `prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`. // - `auth_type` (string): The `auth_type` parameter specifies the requested authentication features (as a comma-separated list), e.g. `reauthenticate`. -func UpstreamParameters(provider Provider, upstreamParameters map[string]string) []oauth2.AuthCodeOption { +func UpstreamParameters(upstreamParameters map[string]string) []oauth2.AuthCodeOption { // validation of upstream parameters are already handled in the `oidc/.schema/link.schema.json` and `oidc/.schema/settings.schema.json` file. // `upstreamParameters` will always only contain allowed parameters based on the configuration. - // we double check the parameters here to prevent any potential security issues. + // we double-check the parameters here to prevent any potential security issues. allowedParameters := map[string]struct{}{ "login_hint": {}, "hd": {}, diff --git a/selfservice/strategy/oidc/provider_auth0.go b/selfservice/strategy/oidc/provider_auth0.go index 15f332d5cfa8..a4c9ee46e1ab 100644 --- a/selfservice/strategy/oidc/provider_auth0.go +++ b/selfservice/strategy/oidc/provider_auth0.go @@ -102,7 +102,7 @@ func (g *ProviderAuth0) Claims(ctx context.Context, exchange *oauth2.Token, quer } // Once auth0 fixes this bug, all this workaround can be removed. - b, err := io.ReadAll(resp.Body) + b, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024)) if err != nil { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) } diff --git a/selfservice/strategy/oidc/provider_config.go b/selfservice/strategy/oidc/provider_config.go index 512a5ecc6fa2..7e2b0b19dbfb 100644 --- a/selfservice/strategy/oidc/provider_config.go +++ b/selfservice/strategy/oidc/provider_config.go @@ -28,6 +28,7 @@ type Configuration struct { // - gitlab // - microsoft // - discord + // - salesforce // - slack // - facebook // - auth0 @@ -71,7 +72,7 @@ type Configuration struct { // SubjectSource is a flag which controls from which endpoint the subject identifier is taken by microsoft provider. // Can be either `userinfo` or `me`. - // If the value is `uerinfo` then the subject identifier is taken from sub field of uderifo standard endpoint response. + // If the value is `userinfo` then the subject identifier is taken from sub field of userinfo standard endpoint response. // If the value is `me` then the `id` field of https://graph.microsoft.com/v1.0/me response is taken as subject. // The default is `userinfo`. SubjectSource string `json:"subject_source"` @@ -118,9 +119,23 @@ type Configuration struct { // endpoint to get the claims) or `id_token` (takes the claims from the id // token). It defaults to `id_token`. ClaimsSource string `json:"claims_source"` + + // PKCE controls if the OpenID Connect OAuth2 flow should use PKCE (Proof Key for Code Exchange). + // Possible values are: `auto` (default), `never`, `force`. + // - `auto`: PKCE is used if the provider supports it. Requires setting `issuer_url`. + // - `never`: Disable PKCE entirely for this provider, even if the provider advertises support for it. + // - `force`: Always use PKCE, even if the provider does not advertise support for it. OAuth2 flows will fail if the provider does not support PKCE. + // IMPORTANT: If you set this to `force`, you must whitelist a different return URL for your OAuth2 client in the provider's configuration. + // Instead of /self-service/methods/oidc/callback/, you must use /self-service/methods/oidc/callback + // (Note the missing path segment and no trailing slash). + PKCE string `json:"pkce"` } func (p Configuration) Redir(public *url.URL) string { + if p.PKCE == "force" { + return urlx.AppendPaths(public, RouteCallbackGeneric).String() + } + if p.OrganizationID != "" { route := RouteOrganizationCallback route = strings.Replace(route, ":provider", p.ID, 1) @@ -141,25 +156,28 @@ type ConfigurationCollection struct { // If you add a provider here, please also add a test to // provider_private_net_test.go var supportedProviders = map[string]func(config *Configuration, reg Dependencies) Provider{ - "generic": NewProviderGenericOIDC, - "google": NewProviderGoogle, - "github": NewProviderGitHub, - "github-app": NewProviderGitHubApp, - "gitlab": NewProviderGitLab, - "microsoft": NewProviderMicrosoft, - "discord": NewProviderDiscord, - "slack": NewProviderSlack, - "facebook": NewProviderFacebook, - "auth0": NewProviderAuth0, - "vk": NewProviderVK, - "yandex": NewProviderYandex, - "apple": NewProviderApple, - "spotify": NewProviderSpotify, - "netid": NewProviderNetID, - "dingtalk": NewProviderDingTalk, - "linkedin": NewProviderLinkedIn, - "patreon": NewProviderPatreon, - "lark": NewProviderLark, + "generic": NewProviderGenericOIDC, + "google": NewProviderGoogle, + "github": NewProviderGitHub, + "github-app": NewProviderGitHubApp, + "gitlab": NewProviderGitLab, + "microsoft": NewProviderMicrosoft, + "discord": NewProviderDiscord, + "salesforce": NewProviderSalesforce, + "slack": NewProviderSlack, + "facebook": NewProviderFacebook, + "auth0": NewProviderAuth0, + "vk": NewProviderVK, + "yandex": NewProviderYandex, + "apple": NewProviderApple, + "spotify": NewProviderSpotify, + "netid": NewProviderNetID, + "dingtalk": NewProviderDingTalk, + "linkedin": NewProviderLinkedIn, + "linkedin_v2": NewProviderLinkedInV2, + "patreon": NewProviderPatreon, + "lark": NewProviderLark, + "x": NewProviderX, } func (c ConfigurationCollection) Provider(id string, reg Dependencies) (Provider, error) { diff --git a/selfservice/strategy/oidc/provider_dingtalk.go b/selfservice/strategy/oidc/provider_dingtalk.go index 36469e6e7c23..12abffe85942 100644 --- a/selfservice/strategy/oidc/provider_dingtalk.go +++ b/selfservice/strategy/oidc/provider_dingtalk.go @@ -65,7 +65,7 @@ func (g *ProviderDingTalk) OAuth2(ctx context.Context) (*oauth2.Config, error) { return g.oauth2(ctx), nil } -func (g *ProviderDingTalk) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) { +func (g *ProviderDingTalk) ExchangeOAuth2Token(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) { conf, err := g.OAuth2(ctx) if err != nil { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) diff --git a/selfservice/strategy/oidc/provider_discord.go b/selfservice/strategy/oidc/provider_discord.go index 181e7df6d322..99bea24d5770 100644 --- a/selfservice/strategy/oidc/provider_discord.go +++ b/selfservice/strategy/oidc/provider_discord.go @@ -93,7 +93,7 @@ func (d *ProviderDiscord) Claims(ctx context.Context, exchange *oauth2.Token, qu Picture: user.AvatarURL(""), Email: user.Email, EmailVerified: x.ConvertibleBoolean(user.Verified), - Locale: user.Locale, + Locale: Locale(user.Locale), } return claims, nil diff --git a/selfservice/strategy/oidc/provider_facebook.go b/selfservice/strategy/oidc/provider_facebook.go index abf9806cce05..8bbca9b24e83 100644 --- a/selfservice/strategy/oidc/provider_facebook.go +++ b/selfservice/strategy/oidc/provider_facebook.go @@ -41,9 +41,9 @@ func NewProviderFacebook( } } -func (g *ProviderFacebook) generateAppSecretProof(ctx context.Context, exchange *oauth2.Token) string { +func (g *ProviderFacebook) generateAppSecretProof(token *oauth2.Token) string { secret := g.config.ClientSecret - data := exchange.AccessToken + data := token.AccessToken h := hmac.New(sha256.New, []byte(secret)) h.Write([]byte(data)) @@ -62,19 +62,19 @@ func (g *ProviderFacebook) OAuth2(ctx context.Context) (*oauth2.Config, error) { return g.oauth2ConfigFromEndpoint(ctx, endpoint), nil } -func (g *ProviderFacebook) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { +func (g *ProviderFacebook) Claims(ctx context.Context, token *oauth2.Token, query url.Values) (*Claims, error) { o, err := g.OAuth2(ctx) if err != nil { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) } - appSecretProof := g.generateAppSecretProof(ctx, exchange) + appSecretProof := g.generateAppSecretProof(token) u, err := url.Parse(fmt.Sprintf("https://graph.facebook.com/me?fields=id,name,first_name,last_name,middle_name,email,picture,birthday,gender&appsecret_proof=%s", appSecretProof)) if err != nil { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) } - ctx, client := httpx.SetOAuth2(ctx, g.reg.HTTPClient(ctx), o, exchange) + ctx, client := httpx.SetOAuth2(ctx, g.reg.HTTPClient(ctx), o, token) req, err := retryablehttp.NewRequestWithContext(ctx, "GET", u.String(), nil) if err != nil { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) diff --git a/selfservice/strategy/oidc/provider_generic_test.go b/selfservice/strategy/oidc/provider_generic_test.go index 221ca7b1827a..7c90da7e3ec5 100644 --- a/selfservice/strategy/oidc/provider_generic_test.go +++ b/selfservice/strategy/oidc/provider_generic_test.go @@ -45,9 +45,9 @@ func makeAuthCodeURL(t *testing.T, r *login.Flow, reg *driver.RegistryDefault) s Mapper: "file://./stub/hydra.schema.json", RequestedClaims: makeOIDCClaims(), }, reg) - c, err := p.OAuth2(context.Background()) + c, err := p.(oidc.OAuth2Provider).OAuth2(context.Background()) require.NoError(t, err) - return c.AuthCodeURL("state", p.AuthCodeURLOptions(r)...) + return c.AuthCodeURL("state", p.(oidc.OAuth2Provider).AuthCodeURLOptions(r)...) } func TestProviderGenericOIDC_AddAuthCodeURLOptions(t *testing.T) { diff --git a/selfservice/strategy/oidc/provider_github_app.go b/selfservice/strategy/oidc/provider_github_app.go index 95801ce59e47..83cfd9bdb882 100644 --- a/selfservice/strategy/oidc/provider_github_app.go +++ b/selfservice/strategy/oidc/provider_github_app.go @@ -15,7 +15,7 @@ import ( "golang.org/x/oauth2" "golang.org/x/oauth2/github" - ghapi "github.com/google/go-github/v27/github" + ghapi "github.com/google/go-github/v38/github" "github.com/ory/herodot" ) @@ -86,7 +86,7 @@ func (g *ProviderGitHubApp) Claims(ctx context.Context, exchange *oauth2.Token, for k, e := range emails { // If it is the primary email or it's the last email (no primary email set?), set the email. - if e.GetPrimary() || k == len(emails) { + if e.GetPrimary() || k == len(emails)-1 { claims.Email = e.GetEmail() claims.EmailVerified = x.ConvertibleBoolean(e.GetVerified()) break diff --git a/selfservice/strategy/oidc/provider_google_test.go b/selfservice/strategy/oidc/provider_google_test.go index d900b088d358..c1a1f65b9348 100644 --- a/selfservice/strategy/oidc/provider_google_test.go +++ b/selfservice/strategy/oidc/provider_google_test.go @@ -34,7 +34,7 @@ func TestProviderGoogle_Scope(t *testing.T) { Scope: []string{"email", "profile", "offline_access"}, }, reg) - c, _ := p.OAuth2(context.Background()) + c, _ := p.(oidc.OAuth2Provider).OAuth2(context.Background()) assert.NotContains(t, c.Scopes, "offline_access") } @@ -55,7 +55,7 @@ func TestProviderGoogle_AccessType(t *testing.T) { ID: x.NewUUID(), } - options := p.AuthCodeURLOptions(r) + options := p.(oidc.OAuth2Provider).AuthCodeURLOptions(r) assert.Contains(t, options, oauth2.AccessTypeOffline) } diff --git a/selfservice/strategy/oidc/provider_linkedin_test.go b/selfservice/strategy/oidc/provider_linkedin_test.go index 5eb7a629c7cc..d5b9df86d25a 100644 --- a/selfservice/strategy/oidc/provider_linkedin_test.go +++ b/selfservice/strategy/oidc/provider_linkedin_test.go @@ -115,7 +115,7 @@ func TestProviderLinkedin_Claims(t *testing.T) { linkedin := oidc.NewProviderLinkedIn(c, reg) const fakeLinkedinIDToken = "id_token_mock_" - actual, err := linkedin.Claims( + actual, err := linkedin.(oidc.OAuth2Provider).Claims( context.Background(), (&oauth2.Token{AccessToken: "foo", Expiry: time.Now().Add(time.Hour)}).WithExtra(map[string]interface{}{"id_token": fakeLinkedinIDToken}), url.Values{}, @@ -191,7 +191,7 @@ func TestProviderLinkedin_No_Picture(t *testing.T) { linkedin := oidc.NewProviderLinkedIn(c, reg) const fakeLinkedinIDToken = "id_token_mock_" - actual, err := linkedin.Claims( + actual, err := linkedin.(oidc.OAuth2Provider).Claims( context.Background(), (&oauth2.Token{AccessToken: "foo", Expiry: time.Now().Add(time.Hour)}).WithExtra(map[string]interface{}{"id_token": fakeLinkedinIDToken}), url.Values{}, diff --git a/selfservice/strategy/oidc/provider_linkedin_v2.go b/selfservice/strategy/oidc/provider_linkedin_v2.go new file mode 100644 index 000000000000..a71d801c24bd --- /dev/null +++ b/selfservice/strategy/oidc/provider_linkedin_v2.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +func NewProviderLinkedInV2( + config *Configuration, + reg Dependencies, +) Provider { + config.ClaimsSource = ClaimsSourceUserInfo + config.IssuerURL = "https://www.linkedin.com/oauth" + + return &ProviderGenericOIDC{ + config: config, + reg: reg, + } +} diff --git a/selfservice/strategy/oidc/provider_linkedin_v2_test.go b/selfservice/strategy/oidc/provider_linkedin_v2_test.go new file mode 100644 index 000000000000..c36e44473fa7 --- /dev/null +++ b/selfservice/strategy/oidc/provider_linkedin_v2_test.go @@ -0,0 +1,34 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/strategy/oidc" +) + +func TestProviderLinkedInV2_Discovery(t *testing.T) { + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + + p := oidc.NewProviderLinkedInV2(&oidc.Configuration{ + Provider: "linkedin_v2", + ID: "valid", + ClientID: "client", + ClientSecret: "secret", + Mapper: "file://./stub/hydra.schema.json", + RequestedClaims: nil, + Scope: []string{"email", "profile", "offline_access"}, + }, reg) + + c, err := p.(oidc.OAuth2Provider).OAuth2(context.Background()) + require.NoError(t, err) + assert.Contains(t, c.Scopes, "openid") + assert.Equal(t, "https://www.linkedin.com/oauth/v2/accessToken", c.Endpoint.TokenURL) +} diff --git a/selfservice/strategy/oidc/provider_private_net_test.go b/selfservice/strategy/oidc/provider_private_net_test.go index 878b8622e993..0505a3e19626 100644 --- a/selfservice/strategy/oidc/provider_private_net_test.go +++ b/selfservice/strategy/oidc/provider_private_net_test.go @@ -73,6 +73,7 @@ func TestProviderPrivateIP(t *testing.T) { // GitHub uses a fixed token URL and does not use the issuer. // GitHub App uses a fixed token URL and does not use the issuer. // GitHub App uses a fixed token URL and does not use the issuer. + // LinkedInV2 uses a fixed token URL and does not use the issuer. {p: gitlab, c: &oidc.Configuration{IssuerURL: "http://127.0.0.2/"}, e: "is not a permitted destination"}, // The TokenURL is fixed in GitLab to {issuer_url}/token. Since the issuer is called first, any local token fails also. @@ -84,10 +85,11 @@ func TestProviderPrivateIP(t *testing.T) { // VK uses a fixed token URL and does not use the issuer. // Yandex uses a fixed token URL and does not use the issuer. // NetID uses a fixed token URL and does not use the issuer. + // X uses a fixed token URL and userinfoRL and does not use the issuer value. } { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { p := tc.p(tc.c) - _, err := p.Claims(context.Background(), (&oauth2.Token{RefreshToken: "foo", Expiry: time.Now().Add(-time.Hour)}).WithExtra(map[string]interface{}{ + _, err := p.(oidc.OAuth2Provider).Claims(context.Background(), (&oauth2.Token{RefreshToken: "foo", Expiry: time.Now().Add(-time.Hour)}).WithExtra(map[string]interface{}{ "id_token": tc.id, }), url.Values{}) require.Error(t, err) diff --git a/selfservice/strategy/oidc/provider_salesforce.go b/selfservice/strategy/oidc/provider_salesforce.go new file mode 100644 index 000000000000..1d028a1a8de7 --- /dev/null +++ b/selfservice/strategy/oidc/provider_salesforce.go @@ -0,0 +1,142 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "encoding/json" + "io" + "net/url" + "path" + "time" + + "github.com/ory/x/httpx" + "github.com/ory/x/stringsx" + + "github.com/tidwall/sjson" + + "github.com/hashicorp/go-retryablehttp" + + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "golang.org/x/oauth2" + + "github.com/ory/herodot" +) + +type ProviderSalesforce struct { + *ProviderGenericOIDC +} + +func NewProviderSalesforce( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderSalesforce{ + ProviderGenericOIDC: &ProviderGenericOIDC{ + config: config, + reg: reg, + }, + } +} + +func (g *ProviderSalesforce) oauth2(ctx context.Context) (*oauth2.Config, error) { + endpoint, err := url.Parse(g.config.IssuerURL) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) + } + + authUrl := *endpoint + tokenUrl := *endpoint + + authUrl.Path = path.Join(authUrl.Path, "/services/oauth2/authorize") + tokenUrl.Path = path.Join(tokenUrl.Path, "/services/oauth2/token") + + c := &oauth2.Config{ + ClientID: g.config.ClientID, + ClientSecret: g.config.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: authUrl.String(), + TokenURL: tokenUrl.String(), + }, + Scopes: g.config.Scope, + RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)), + } + + return c, nil +} + +func (g *ProviderSalesforce) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return g.oauth2(ctx) +} + +func (g *ProviderSalesforce) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { + o, err := g.OAuth2(ctx) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) + } + + u, err := url.Parse(g.config.IssuerURL) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) + } + u.Path = path.Join(u.Path, "/services/oauth2/userinfo") + + ctx, client := httpx.SetOAuth2(ctx, g.reg.HTTPClient(ctx), o, exchange) + req, err := retryablehttp.NewRequestWithContext(ctx, "GET", u.String(), nil) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) + } + + req.Header.Add("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) + } + defer resp.Body.Close() + + if err := logUpstreamError(g.reg.Logger(), resp); err != nil { + return nil, err + } + + // Once Salesforce fixes this bug, all this workaround can be removed. + b, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024)) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) + } + + b, err = salesforceUpdatedAtWorkaround(b) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) + } + + // Once we get here, we know that if there is an updated_at field in the json, it is the correct type. + var claims Claims + if err := json.Unmarshal(b, &claims); err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) + } + + claims.Issuer = stringsx.Coalesce(claims.Issuer, g.config.IssuerURL) + return &claims, nil +} + +// There is a bug in the response from Salesforce. The updated_at field may be a string and not an int64. +// https://help.salesforce.com/s/articleView?id=sf.remoteaccess_using_userinfo_endpoint.htm&type=5 +// We work around this by reading the json generically (as map[string]inteface{} and looking at the updated_at field +// if it exists. If it's the wrong type (string), we fill out the claims by hand. +func salesforceUpdatedAtWorkaround(body []byte) ([]byte, error) { + // Force updatedAt to be an int if given as a string in the response. + if updatedAtField := gjson.GetBytes(body, "updated_at"); updatedAtField.Exists() && updatedAtField.Type == gjson.String { + t, err := time.Parse(time.RFC3339, updatedAtField.String()) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("bad time format in updated_at")) + } + body, err = sjson.SetBytes(body, "updated_at", t.Unix()) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) + } + } + return body, nil +} diff --git a/selfservice/strategy/oidc/provider_salesforce_unit_test.go b/selfservice/strategy/oidc/provider_salesforce_unit_test.go new file mode 100644 index 000000000000..81638fb18050 --- /dev/null +++ b/selfservice/strategy/oidc/provider_salesforce_unit_test.go @@ -0,0 +1,33 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSalesforceUpdatedAtWorkaround(t *testing.T) { + actual, err := salesforceUpdatedAtWorkaround([]byte("{}")) + require.NoError(t, err) + assert.Equal(t, "{}", string(actual)) + + actual, err = salesforceUpdatedAtWorkaround([]byte(`{"updated_at":1234}`)) + require.NoError(t, err) + assert.Equal(t, `{"updated_at":1234}`, string(actual)) + + timestamp := time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC) + input, err := json.Marshal(map[string]interface{}{ + "updated_at": timestamp, + }) + require.NoError(t, err) + actual, err = salesforceUpdatedAtWorkaround(input) + require.NoError(t, err) + assert.Equal(t, fmt.Sprintf(`{"updated_at":%d}`, timestamp.Unix()), string(actual)) +} diff --git a/selfservice/strategy/oidc/provider_test.go b/selfservice/strategy/oidc/provider_test.go index 7c0de7c55138..208421ad2ab0 100644 --- a/selfservice/strategy/oidc/provider_test.go +++ b/selfservice/strategy/oidc/provider_test.go @@ -9,6 +9,7 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -31,18 +32,18 @@ func NewTestProvider(c *Configuration, reg Dependencies) Provider { } } -func RegisterTestProvider(id string) func() { +func RegisterTestProvider(t *testing.T, id string) { supportedProviders[id] = func(c *Configuration, reg Dependencies) Provider { return NewTestProvider(c, reg) } - return func() { + t.Cleanup(func() { delete(supportedProviders, id) - } + }) } var _ IDTokenVerifier = new(TestProvider) -func (t *TestProvider) Verify(ctx context.Context, token string) (*Claims, error) { +func (t *TestProvider) Verify(_ context.Context, token string) (*Claims, error) { if token == "error" { return nil, fmt.Errorf("stub error") } @@ -52,3 +53,56 @@ func (t *TestProvider) Verify(ctx context.Context, token string) (*Claims, error } return &c, nil } + +func TestLocale(t *testing.T) { + // test json unmarshal + for _, tc := range []struct { + name string + json string + expected string + assertErr assert.ErrorAssertionFunc + }{{ + name: "empty", + json: `{}`, + expected: "", + }, { + name: "empty string locale", + json: `{"locale":""}`, + expected: "", + }, { + name: "invalid string locale", + json: `{"locale":"""}`, + assertErr: assert.Error, + }, { + name: "string locale", + json: `{"locale":"en-US"}`, + expected: "en-US", + }, { + name: "linkedin locale", + json: `{"locale":{"country":"US","language":"en","ignore":"me"}}`, + expected: "en-US", + }, { + name: "missing country linkedin locale", + json: `{"locale":{"language":"en"}}`, + expected: "en", + }, { + name: "missing language linkedin locale", + json: `{"locale":{"country":"US"}}`, + expected: "US", + }, { + name: "invalid linkedin locale", + json: `{"locale":{"invalid":"me"}}`, + expected: "", + }} { + t.Run(tc.name, func(t *testing.T) { + var c Claims + err := json.Unmarshal([]byte(tc.json), &c) + if tc.assertErr != nil { + tc.assertErr(t, err) + return + } + require.NoError(t, err) + assert.EqualValues(t, tc.expected, c.Locale) + }) + } +} diff --git a/selfservice/strategy/oidc/provider_userinfo_test.go b/selfservice/strategy/oidc/provider_userinfo_test.go index 0b11f2dcae90..dde2507af319 100644 --- a/selfservice/strategy/oidc/provider_userinfo_test.go +++ b/selfservice/strategy/oidc/provider_userinfo_test.go @@ -89,6 +89,16 @@ func TestProviderClaimsRespectsErrorCodes(t *testing.T) { Provider: "auth0", }, reg), }, + { + name: "salesforce", + userInfoHandler: defaultUserinfoHandler, + userInfoEndpoint: "https://www.salesforce.com/services/oauth2/userinfo", + provider: oidc.NewProviderSalesforce(&oidc.Configuration{ + IssuerURL: "https://www.salesforce.com", + ID: "salesforce", + Provider: "salesforce", + }, reg), + }, { name: "netid", userInfoHandler: defaultUserinfoHandler, @@ -343,7 +353,7 @@ func TestProviderClaimsRespectsErrorCodes(t *testing.T) { return resp, err }) - _, err := tc.provider.Claims(ctx, token, url.Values{}) + _, err := tc.provider.(oidc.OAuth2Provider).Claims(ctx, token, url.Values{}) var he *herodot.DefaultError require.ErrorAs(t, err, &he) assert.Equal(t, "OpenID Connect provider returned a 455 status code but 200 is expected.", he.Reason()) @@ -359,7 +369,7 @@ func TestProviderClaimsRespectsErrorCodes(t *testing.T) { httpmock.RegisterResponder("GET", tc.userInfoEndpoint, tc.userInfoHandler) - claims, err := tc.provider.Claims(ctx, token, url.Values{}) + claims, err := tc.provider.(oidc.OAuth2Provider).Claims(ctx, token, url.Values{}) require.NoError(t, err) if tc.expectedClaims == nil { assert.Equal(t, expectedClaims, claims) diff --git a/selfservice/strategy/oidc/provider_x.go b/selfservice/strategy/oidc/provider_x.go new file mode 100644 index 000000000000..f58dbd48182f --- /dev/null +++ b/selfservice/strategy/oidc/provider_x.go @@ -0,0 +1,171 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/ory/x/otelx" + + "github.com/dghubble/oauth1" + "github.com/dghubble/oauth1/twitter" + "github.com/pkg/errors" + + "github.com/ory/herodot" +) + +var _ Provider = (*ProviderX)(nil) +var _ OAuth1Provider = (*ProviderX)(nil) + +const xUserInfoBase = "https://api.twitter.com/1.1/account/verify_credentials.json" +const xUserInfoWithEmail = xUserInfoBase + "?include_email=true" + +type ProviderX struct { + config *Configuration + reg Dependencies +} + +func (p *ProviderX) Config() *Configuration { + return p.config +} + +func NewProviderX( + config *Configuration, + reg Dependencies) Provider { + return &ProviderX{ + config: config, + reg: reg, + } +} + +func (p *ProviderX) ExchangeToken(ctx context.Context, req *http.Request) (*oauth1.Token, error) { + requestToken, verifier, err := oauth1.ParseAuthorizationCallback(req) + if err != nil { + return nil, err + } + + accessToken, accessSecret, err := p.OAuth1(ctx).AccessToken(requestToken, "", verifier) + if err != nil { + return nil, err + } + + return oauth1.NewToken(accessToken, accessSecret), nil +} + +func (p *ProviderX) AuthURL(ctx context.Context, state string) (_ string, err error) { + ctx, span := p.reg.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.ProviderLinkedIn.fetch") + defer otelx.End(span, &err) + + c := p.OAuth1(ctx) + + // We need to cheat so that callback validates on return + c.CallbackURL = c.CallbackURL + fmt.Sprintf("?state=%s&code=unused", state) + + requestToken, _, err := c.RequestToken() + if err != nil { + span.RecordError(err) + return "", errors.WithStack(herodot.ErrInternalServerError.WithWrap(err).WithReasonf(`Unable to sign in with X because the OAuth1 request token could not be initialized: %s`, err)) + } + + authzURL, err := c.AuthorizationURL(requestToken) + if err != nil { + span.RecordError(err) + return "", errors.WithStack(herodot.ErrInternalServerError.WithWrap(err).WithReasonf(`Unable to sign in with X because the OAuth1 authorization URL could not be parsed: %s`, err)) + } + + return authzURL.String(), nil +} + +func (p *ProviderX) CheckError(ctx context.Context, r *http.Request) error { + if r.URL.Query().Get("denied") == "" { + return nil + } + + return errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to sign in with X because the user denied the request.`)) +} + +func (p *ProviderX) OAuth1(ctx context.Context) *oauth1.Config { + return &oauth1.Config{ + ConsumerKey: p.config.ClientID, + ConsumerSecret: p.config.ClientSecret, + Endpoint: twitter.AuthenticateEndpoint, + CallbackURL: p.config.Redir(p.reg.Config().OIDCRedirectURIBase(ctx)), + } +} + +func (p *ProviderX) userInfoEndpoint() string { + for _, scope := range p.config.Scope { + if scope == "email" { + return xUserInfoWithEmail + } + } + + return xUserInfoBase +} + +func (p *ProviderX) Claims(ctx context.Context, token *oauth1.Token) (*Claims, error) { + ctx = context.WithValue(ctx, oauth1.HTTPClient, p.reg.HTTPClient(ctx).HTTPClient) + + c := p.OAuth1(ctx) + client := c.Client(ctx, token) + endpoint := p.userInfoEndpoint() + + resp, err := client.Get(endpoint) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) + } + defer resp.Body.Close() + + if err := logUpstreamError(p.reg.Logger(), resp); err != nil { + return nil, err + } + + user := &xUser{} + if err := json.NewDecoder(resp.Body).Decode(user); err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) + } + + website := "" + if user.URL != nil { + website = *user.URL + } + + return &Claims{ + Issuer: endpoint, + Subject: user.IDStr, + Name: user.Name, + Picture: user.ProfileImageURLHTTPS, + Email: user.Email, + PreferredUsername: user.ScreenName, + Website: website, + }, nil +} + +type xUser struct { + ID int `json:"id"` + IDStr string `json:"id_str"` + Name string `json:"name"` + ScreenName string `json:"screen_name"` + Location string `json:"location"` + Description string `json:"description"` + URL *string `json:"url,omitempty"` + Protected bool `json:"protected"` + FollowersCount int `json:"followers_count"` + FriendsCount int `json:"friends_count"` + ListedCount int `json:"listed_count"` + CreatedAt string `json:"created_at"` + FavouritesCount int `json:"favourites_count"` + Verified bool `json:"verified"` + StatusesCount int `json:"statuses_count"` + DefaultProfile bool `json:"default_profile"` + DefaultProfileImage bool `json:"default_profile_image"` + ProfileImageURLHTTPS string `json:"profile_image_url_https"` + WithheldInCountries []string `json:"withheld_in_countries"` + Suspended bool `json:"suspended"` + NeedsPhoneVerification bool `json:"needs_phone_verification"` + Email string `json:"email"` +} diff --git a/selfservice/strategy/oidc/state.go b/selfservice/strategy/oidc/state.go new file mode 100644 index 000000000000..ce8cb17bc653 --- /dev/null +++ b/selfservice/strategy/oidc/state.go @@ -0,0 +1,63 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "crypto/sha512" + "crypto/subtle" + + "github.com/gofrs/uuid" + "golang.org/x/oauth2" + "google.golang.org/protobuf/proto" + + "github.com/ory/herodot" + "github.com/ory/kratos/cipher" + oidcv1 "github.com/ory/kratos/gen/oidc/v1" + "github.com/ory/kratos/x" +) + +func encryptState(ctx context.Context, c cipher.Cipher, state *oidcv1.State) (ciphertext string, err error) { + m, err := proto.Marshal(state) + if err != nil { + return "", herodot.ErrInternalServerError.WithReasonf("Unable to marshal state: %s", err) + } + return c.Encrypt(ctx, m) +} + +func DecryptState(ctx context.Context, c cipher.Cipher, ciphertext string) (*oidcv1.State, error) { + plaintext, err := c.Decrypt(ctx, ciphertext) + if err != nil { + return nil, herodot.ErrBadRequest.WithReasonf("Unable to decrypt state: %s", err) + } + var state oidcv1.State + if err := proto.Unmarshal(plaintext, &state); err != nil { + return nil, herodot.ErrBadRequest.WithReasonf("Unable to unmarshal state: %s", err) + } + return &state, nil +} + +func (s *Strategy) GenerateState(ctx context.Context, p Provider, flowID uuid.UUID) (stateParam string, pkce []oauth2.AuthCodeOption, err error) { + state := oidcv1.State{ + FlowId: flowID.Bytes(), + SessionTokenExchangeCodeSha512: x.NewUUID().Bytes(), + ProviderId: p.Config().ID, + PkceVerifier: maybePKCE(ctx, s.d, p), + } + if code, hasCode, _ := s.d.SessionTokenExchangePersister().CodeForFlow(ctx, flowID); hasCode { + sum := sha512.Sum512([]byte(code.InitCode)) + state.SessionTokenExchangeCodeSha512 = sum[:] + } + + param, err := encryptState(ctx, s.d.Cipher(ctx), &state) + if err != nil { + return "", nil, herodot.ErrInternalServerError.WithReason("Unable to encrypt state").WithWrap(err) + } + return param, PKCEChallenge(&state), nil +} + +func codeMatches(s *oidcv1.State, code string) bool { + sum := sha512.Sum512([]byte(code)) + return subtle.ConstantTimeCompare(s.GetSessionTokenExchangeCodeSha512(), sum[:]) == 1 +} diff --git a/selfservice/strategy/oidc/state_test.go b/selfservice/strategy/oidc/state_test.go new file mode 100644 index 000000000000..bf0366571a6b --- /dev/null +++ b/selfservice/strategy/oidc/state_test.go @@ -0,0 +1,46 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/cipher" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/x" +) + +func TestGenerateState(t *testing.T) { + conf, reg := internal.NewFastRegistryWithMocks(t) + _ = conf + strat := oidc.NewStrategy(reg) + ctx := context.Background() + ciph := reg.Cipher(ctx) + _, ok := ciph.(*cipher.Noop) + require.False(t, ok) + + flowID := x.NewUUID() + + stateParam, pkce, err := strat.GenerateState(ctx, &testProvider{}, flowID) + require.NoError(t, err) + require.NotEmpty(t, stateParam) + assert.Empty(t, pkce) + + state, err := oidc.DecryptState(ctx, ciph, stateParam) + require.NoError(t, err) + assert.Equal(t, flowID.Bytes(), state.FlowId) + assert.Empty(t, oidc.PKCEVerifier(state)) + assert.Equal(t, "test-provider", state.ProviderId) +} + +type testProvider struct{} + +func (t *testProvider) Config() *oidc.Configuration { + return &oidc.Configuration{ID: "test-provider", PKCE: "never"} +} diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 46bcec8ad47d..8901c0ee8e09 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -6,14 +6,17 @@ package oidc import ( "bytes" "context" - "crypto/sha512" - "encoding/base64" "encoding/json" - "fmt" "net/http" "net/url" "path/filepath" + "slices" "strings" + "time" + + "github.com/ory/x/sqlxx" + + "golang.org/x/exp/maps" "github.com/ory/x/urlx" @@ -21,7 +24,7 @@ import ( "golang.org/x/oauth2" "github.com/ory/kratos/cipher" - "github.com/ory/kratos/selfservice/flowhelpers" + oidcv1 "github.com/ory/kratos/gen/oidc/v1" "github.com/ory/kratos/selfservice/sessiontokenexchange" "github.com/ory/x/jsonnetsecure" "github.com/ory/x/otelx" @@ -62,6 +65,7 @@ const ( RouteAuth = RouteBase + "/auth/:flow" RouteCallback = RouteBase + "/callback/:provider" + RouteCallbackGeneric = RouteBase + "/callback" RouteOrganizationCallback = RouteBase + "/organization/:organization/callback/:provider" ) @@ -116,9 +120,9 @@ type Dependencies interface { func isForced(req interface{}) bool { f, ok := req.(interface { - IsForced() bool + IsRefresh() bool }) - return ok && f.IsForced() + return ok && f.IsRefresh() } // Strategy implements selfservice.LoginStrategy, selfservice.RegistrationStrategy and selfservice.SettingsStrategy. @@ -139,43 +143,7 @@ type AuthCodeContainer struct { // fandom-end } -type State struct { - FlowID string - Data []byte -} - -func (s *State) String() string { - return base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", s.FlowID, s.Data))) -} - -func generateState(flowID string) *State { - return &State{ - FlowID: flowID, - Data: x.NewUUID().Bytes(), - } -} - -func (s *State) setCode(code string) { - s.Data = sha512.New().Sum([]byte(code)) -} - -func (s *State) codeMatches(code string) bool { - return bytes.Equal(s.Data, sha512.New().Sum([]byte(code))) -} - -func parseState(s string) (*State, error) { - raw, err := base64.RawURLEncoding.DecodeString(s) - if err != nil { - return nil, err - } - if id, data, ok := bytes.Cut(raw, []byte(":")); !ok { - return nil, errors.New("state has invalid format") - } else { - return &State{FlowID: string(id), Data: data}, nil - } -} - -func (s *Strategy) CountActiveFirstFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { +func (s *Strategy) CountActiveFirstFactorCredentials(_ context.Context, cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { for _, c := range cc { if c.Type == s.ID() && gjson.ValidBytes(c.Config) { var conf identity.CredentialsOIDC @@ -200,7 +168,7 @@ func (s *Strategy) CountActiveFirstFactorCredentials(cc map[identity.Credentials return } -func (s *Strategy) CountActiveMultiFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { +func (s *Strategy) CountActiveMultiFactorCredentials(_ context.Context, _ map[identity.CredentialsType]identity.Credentials) (count int, err error) { return 0, nil } @@ -209,6 +177,9 @@ func (s *Strategy) setRoutes(r *x.RouterPublic) { if handle, _, _ := r.Lookup("GET", RouteCallback); handle == nil { r.GET(RouteCallback, wrappedHandleCallback) } + if handle, _, _ := r.Lookup("GET", RouteCallbackGeneric); handle == nil { + r.GET(RouteCallbackGeneric, wrappedHandleCallback) + } // Apple can use the POST request method when calling the callback if handle, _, _ := r.Lookup("POST", RouteCallback); handle == nil { @@ -290,7 +261,7 @@ func (s *Strategy) validateFlow(ctx context.Context, r *http.Request, rid uuid.U return ar, err // this must return the error } -func (s *Strategy) ValidateCallback(w http.ResponseWriter, r *http.Request) (flow.Flow, *AuthCodeContainer, error) { +func (s *Strategy) ValidateCallback(w http.ResponseWriter, r *http.Request, ps httprouter.Params) (flow.Flow, *oidcv1.State, *AuthCodeContainer, error) { var ( codeParam = stringsx.Coalesce(r.URL.Query().Get("code"), r.URL.Query().Get("authCode")) stateParam = r.URL.Query().Get("state") @@ -298,48 +269,67 @@ func (s *Strategy) ValidateCallback(w http.ResponseWriter, r *http.Request) (flo ) if stateParam == "" { - return nil, nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the OpenID Provider did not return the state query parameter.`)) + return nil, nil, nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the OpenID Provider did not return the state query parameter.`)) } - state, err := parseState(stateParam) + state, err := DecryptState(r.Context(), s.d.Cipher(r.Context()), stateParam) if err != nil { - return nil, nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the state parameter was invalid.`)) + return nil, nil, nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the state parameter is invalid.`)) + } + + if providerFromURL := ps.ByName("provider"); providerFromURL != "" { + // We're serving an OIDC callback URL with provider in the URL. + if state.ProviderId == "" { + // provider in URL, but not in state: compatiblity mode, remove this fallback later + state.ProviderId = providerFromURL + } else if state.ProviderId != providerFromURL { + // provider in state, but URL with different provider -> something's fishy + return nil, nil, nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow: provider mismatch between internal state and URL.`)) + } + } + if state.ProviderId == "" { + // weird: provider neither in the state nor in the URL + return nil, nil, nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow: provider could not be retrieved from state nor URL.`)) } - f, err := s.validateFlow(r.Context(), r, x.ParseUUID(state.FlowID)) + f, err := s.validateFlow(r.Context(), r, uuid.FromBytesOrNil(state.FlowId)) if err != nil { - return nil, nil, err + return nil, state, nil, err } tokenCode, hasSessionTokenCode, err := s.d.SessionTokenExchangePersister().CodeForFlow(r.Context(), f.GetID()) if err != nil { - return nil, nil, err + return nil, state, nil, err } cntnr := AuthCodeContainer{} if f.GetType() == flow.TypeBrowser || !hasSessionTokenCode { - if _, err := s.d.ContinuityManager().Continue(r.Context(), w, r, sessionName, continuity.WithPayload(&cntnr)); err != nil { - return nil, nil, err + if _, err := s.d.ContinuityManager().Continue(r.Context(), w, r, sessionName, + continuity.WithPayload(&cntnr), + continuity.WithExpireInsteadOfDelete(time.Minute), + ); err != nil { + return nil, state, nil, err } if stateParam != cntnr.State { - return nil, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the query state parameter does not match the state parameter from the session cookie.`)) + return nil, state, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the query state parameter does not match the state parameter from the session cookie.`)) } } else { // We need to validate the tokenCode here - if !state.codeMatches(tokenCode.InitCode) { - return nil, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the query state parameter does not match the state parameter from the code.`)) + if !codeMatches(state, tokenCode.InitCode) { + return nil, state, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the query state parameter does not match the state parameter from the code.`)) } cntnr.State = stateParam - cntnr.FlowID = state.FlowID + cntnr.FlowID = uuid.FromBytesOrNil(state.FlowId).String() } if errorParam != "" { - return f, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the OpenID Provider returned error "%s": %s`, r.URL.Query().Get("error"), r.URL.Query().Get("error_description"))) + return f, state, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the OpenID Provider returned error "%s": %s`, r.URL.Query().Get("error"), r.URL.Query().Get("error_description"))) } + if codeParam == "" { - return f, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the OpenID Provider did not return the code query parameter.`)) + return f, state, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the OpenID Provider did not return the code query parameter.`)) } - return f, &cntnr, nil + return f, state, &cntnr, nil } func registrationOrLoginFlowID(flow any) (uuid.UUID, bool) { @@ -353,9 +343,7 @@ func registrationOrLoginFlowID(flow any) (uuid.UUID, bool) { } } -func (s *Strategy) alreadyAuthenticated(w http.ResponseWriter, r *http.Request, f interface{}) (bool, error) { - ctx := r.Context() - +func (s *Strategy) alreadyAuthenticated(ctx context.Context, w http.ResponseWriter, r *http.Request, f interface{}) (bool, error) { if sess, _ := s.d.SessionManager().FetchFromRequest(ctx, r); sess != nil { if _, ok := f.(*settings.Flow); ok { // ignore this if it's a settings flow @@ -371,7 +359,7 @@ func (s *Strategy) alreadyAuthenticated(w http.ResponseWriter, r *http.Request, returnTo := s.d.Config().SelfServiceBrowserDefaultReturnTo(ctx) if redirecter, ok := f.(flow.FlowWithRedirect); ok { r, err := x.SecureRedirectTo(r, returnTo, redirecter.SecureRedirectToOpts(ctx, s.d)...) - if err != nil { + if err == nil { returnTo = r } } @@ -386,148 +374,157 @@ func (s *Strategy) alreadyAuthenticated(w http.ResponseWriter, r *http.Request, func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { var ( code = stringsx.Coalesce(r.URL.Query().Get("code"), r.URL.Query().Get("authCode")) - pid = ps.ByName("provider") + err error ) - ctx := r.Context() - ctx = context.WithValue(ctx, httprouter.ParamsKey, ps) + ctx := context.WithValue(r.Context(), httprouter.ParamsKey, ps) + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "strategy.oidc.ExchangeCode") + defer otelx.End(span, &err) r = r.WithContext(ctx) - req, cntnr, err := s.ValidateCallback(w, r) + req, state, cntnr, err := s.ValidateCallback(w, r, ps) if err != nil { if req != nil { - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + s.forwardError(ctx, w, r, req, s.handleError(ctx, w, r, req, state.ProviderId, nil, err)) } else { - s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, s.handleError(w, r, nil, pid, nil, err)) + s.d.SelfServiceErrorManager().Forward(ctx, w, r, s.handleError(ctx, w, r, nil, "", nil, err)) } return } - if authenticated, err := s.alreadyAuthenticated(w, r, req); err != nil { - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + if authenticated, err := s.alreadyAuthenticated(ctx, w, r, req); err != nil { + s.forwardError(ctx, w, r, req, s.handleError(ctx, w, r, req, state.ProviderId, nil, err)) } else if authenticated { return } - provider, err := s.provider(r.Context(), r, pid) + provider, err := s.provider(ctx, state.ProviderId) if err != nil { - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + s.forwardError(ctx, w, r, req, s.handleError(ctx, w, r, req, state.ProviderId, nil, err)) return } - token, err := s.ExchangeCode(r.Context(), provider, code) - if err != nil { - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) - return - } + var claims *Claims + var et *identity.CredentialsOIDCEncryptedTokens + switch p := provider.(type) { + case OAuth2Provider: + token, err := s.ExchangeCode(ctx, provider, code, PKCEVerifier(state)) + if err != nil { + s.forwardError(ctx, w, r, req, s.handleError(ctx, w, r, req, state.ProviderId, nil, err)) + return + } - claims, err := provider.Claims(r.Context(), token, r.URL.Query()) - if err != nil { - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) - return + et, err = s.encryptOAuth2Tokens(ctx, token) + if err != nil { + s.forwardError(ctx, w, r, req, s.handleError(ctx, w, r, req, state.ProviderId, nil, err)) + return + } + + claims, err = p.Claims(ctx, token, r.URL.Query()) + if err != nil { + s.forwardError(ctx, w, r, req, s.handleError(ctx, w, r, req, state.ProviderId, nil, err)) + return + } + case OAuth1Provider: + token, err := p.ExchangeToken(ctx, r) + if err != nil { + s.forwardError(ctx, w, r, req, s.handleError(ctx, w, r, req, state.ProviderId, nil, err)) + return + } + + claims, err = p.Claims(ctx, token) + if err != nil { + s.forwardError(ctx, w, r, req, s.handleError(ctx, w, r, req, state.ProviderId, nil, err)) + return + } } - if err := claims.Validate(); err != nil { - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + if err = claims.Validate(); err != nil { + s.forwardError(ctx, w, r, req, s.handleError(ctx, w, r, req, state.ProviderId, nil, err)) return } + span.SetAttributes(attribute.StringSlice("claims", maps.Keys(claims.RawClaims))) + switch a := req.(type) { case *login.Flow: - if ff, err := s.processLogin(w, r, a, token, claims, provider, cntnr); err != nil { + a.Active = s.ID() + a.TransientPayload = cntnr.TransientPayload + if ff, err := s.processLogin(ctx, w, r, a, et, claims, provider, cntnr); err != nil { + if errors.Is(err, flow.ErrCompletedByStrategy) { + return + } if ff != nil { - s.forwardError(w, r, ff, err) + s.forwardError(ctx, w, r, ff, err) return } - s.forwardError(w, r, a, err) + s.forwardError(ctx, w, r, a, err) } return case *registration.Flow: + a.Active = s.ID() a.TransientPayload = cntnr.TransientPayload - if ff, err := s.processRegistration(w, r, a, token, claims, provider, cntnr, ""); err != nil { + if ff, err := s.processRegistration(ctx, w, r, a, et, claims, provider, cntnr); err != nil { if ff != nil { - s.forwardError(w, r, ff, err) + s.forwardError(ctx, w, r, ff, err) return } - s.forwardError(w, r, a, err) + s.forwardError(ctx, w, r, a, err) } return case *settings.Flow: - sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r) + a.Active = sqlxx.NullString(s.ID()) + a.TransientPayload = cntnr.TransientPayload + sess, err := s.d.SessionManager().FetchFromRequest(ctx, r) if err != nil { - s.forwardError(w, r, a, s.handleError(w, r, a, pid, nil, err)) + s.forwardError(ctx, w, r, a, s.handleError(ctx, w, r, a, state.ProviderId, nil, err)) return } - if err := s.linkProvider(w, r, &settings.UpdateContext{Session: sess, Flow: a}, token, claims, provider); err != nil { - s.forwardError(w, r, a, s.handleError(w, r, a, pid, nil, err)) + if err := s.linkProvider(w, r, &settings.UpdateContext{Session: sess, Flow: a}, et, claims, provider); err != nil { + s.forwardError(ctx, w, r, a, s.handleError(ctx, w, r, a, state.ProviderId, nil, err)) return } return default: - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, errors.WithStack(x.PseudoPanic. + s.forwardError(ctx, w, r, req, s.handleError(ctx, w, r, req, state.ProviderId, nil, errors.WithStack(x.PseudoPanic. WithDetailf("cause", "Unexpected type in OpenID Connect flow: %T", a)))) return } } -func (s *Strategy) ExchangeCode(ctx context.Context, provider Provider, code string) (token *oauth2.Token, err error) { +func (s *Strategy) ExchangeCode(ctx context.Context, provider Provider, code string, opts []oauth2.AuthCodeOption) (token *oauth2.Token, err error) { ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "strategy.oidc.ExchangeCode") defer otelx.End(span, &err) span.SetAttributes(attribute.String("provider_id", provider.Config().ID)) span.SetAttributes(attribute.String("provider_label", provider.Config().Label)) - te, ok := provider.(TokenExchanger) - if !ok { - te, err = provider.OAuth2(ctx) - if err != nil { - return nil, err + switch p := provider.(type) { + case OAuth2Provider: + te, ok := provider.(OAuth2TokenExchanger) + if !ok { + te, err = p.OAuth2(ctx) + if err != nil { + return nil, err + } } - } - client := s.d.HTTPClient(ctx) - ctx = context.WithValue(ctx, oauth2.HTTPClient, client.HTTPClient) - token, err = te.Exchange(ctx, code) - return token, err + client := s.d.HTTPClient(ctx) + ctx = context.WithValue(ctx, oauth2.HTTPClient, client.HTTPClient) + token, err = te.Exchange(ctx, code, opts...) + return token, err + default: + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The chosen provider is not capable of exchanging an OAuth 2.0 code for an access token.")) + } } -func (s *Strategy) populateMethod(r *http.Request, f flow.Flow, message func(provider string) *text.Message) error { +func (s *Strategy) populateMethod(r *http.Request, f flow.Flow, message func(provider string, providerId string) *text.Message) error { conf, err := s.Config(r.Context()) if err != nil { return err } - providers := conf.Providers - - if lf, ok := f.(*login.Flow); ok && lf.IsForced() { - if _, id, c := flowhelpers.GuessForcedLoginIdentifier(r, s.d, lf, s.ID()); id != nil { - if c == nil { - // no OIDC credentials, don't add any providers - providers = nil - } else { - var credentials identity.CredentialsOIDC - if err := json.Unmarshal(c.Config, &credentials); err != nil { - // failed to read OIDC credentials, don't add any providers - providers = nil - } else { - // add only providers that can actually be used to log in as this identity - providers = make([]Configuration, 0, len(conf.Providers)) - for i := range conf.Providers { - for j := range credentials.Providers { - if conf.Providers[i].ID == credentials.Providers[j].Provider { - providers = append(providers, conf.Providers[i]) - break - } - } - } - } - } - } - } - - // does not need sorting because there is only one field - c := f.GetUI() - c.SetCSRF(s.d.GenerateCSRFToken(r)) - AddProviders(c, providers, message) + f.GetUI().SetCSRF(s.d.GenerateCSRFToken(r)) + AddProviders(f.GetUI(), conf.Providers, message) return nil } @@ -546,7 +543,7 @@ func (s *Strategy) Config(ctx context.Context) (*ConfigurationCollection, error) return &c, nil } -func (s *Strategy) provider(ctx context.Context, r *http.Request, id string) (Provider, error) { +func (s *Strategy) provider(ctx context.Context, id string) (Provider, error) { if c, err := s.Config(ctx); err != nil { return nil, err } else if provider, err := c.Provider(id, s.d); err != nil { @@ -556,7 +553,7 @@ func (s *Strategy) provider(ctx context.Context, r *http.Request, id string) (Pr } } -func (s *Strategy) forwardError(w http.ResponseWriter, r *http.Request, f flow.Flow, err error) { +func (s *Strategy) forwardError(ctx context.Context, w http.ResponseWriter, r *http.Request, f flow.Flow, err error) { switch ff := f.(type) { case *login.Flow: s.d.LoginFlowErrorHandler().WriteFlowError(w, r, ff, s.NodeGroup(), err) @@ -564,7 +561,7 @@ func (s *Strategy) forwardError(w http.ResponseWriter, r *http.Request, f flow.F s.d.RegistrationFlowErrorHandler().WriteFlowError(w, r, ff, s.NodeGroup(), err) case *settings.Flow: var i *identity.Identity - if sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r); err == nil { + if sess, err := s.d.SessionManager().FetchFromRequest(ctx, r); err == nil { i = sess.Identity } s.d.SettingsFlowErrorHandler().WriteFlowError(w, r, s.NodeGroup(), ff, i, err) @@ -573,7 +570,7 @@ func (s *Strategy) forwardError(w http.ResponseWriter, r *http.Request, f flow.F } } -func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Flow, providerID string, traits []byte, err error) error { +func (s *Strategy) handleError(ctx context.Context, w http.ResponseWriter, r *http.Request, f flow.Flow, usedProviderID string, traits []byte, err error) error { switch rf := f.(type) { case *login.Flow: return err @@ -593,38 +590,28 @@ func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Fl rf.UI.Messages.Add(text.NewErrorValidationDuplicateCredentialsOnOIDCLink()) } - lf, err := s.registrationToLogin(w, r, rf, providerID) + lf, err := s.registrationToLogin(ctx, w, r, rf) if err != nil { return err } // return a new login flow with the error message embedded in the login flow. - redirectURL := lf.AppendTo(s.d.Config().SelfServiceFlowLoginUI(r.Context())) - if dc, err := flow.DuplicateCredentials(lf); err == nil && dc != nil { - redirectURL = urlx.CopyWithQuery(redirectURL, url.Values{"no_org_ui": {"true"}}) - - for i, n := range lf.UI.Nodes { - if n.Meta == nil || n.Meta.Label == nil { - continue - } - switch n.Meta.Label.ID { - case text.InfoSelfServiceLogin: - lf.UI.Nodes[i].Meta.Label = text.NewInfoLoginAndLink() - case text.InfoSelfServiceLoginWith: - p := gjson.GetBytes(n.Meta.Label.Context, "provider").String() - lf.UI.Nodes[i].Meta.Label = text.NewInfoLoginWithAndLink(p) + var redirectURL *url.URL + if lf.Type == flow.TypeAPI { + returnTo := s.d.Config().SelfServiceBrowserDefaultReturnTo(ctx) + if redirecter, ok := f.(flow.FlowWithRedirect); ok { + secureReturnTo, err := x.SecureRedirectTo(r, returnTo, redirecter.SecureRedirectToOpts(ctx, s.d)...) + if err == nil { + returnTo = secureReturnTo } } - - newLoginURL := s.d.Config().SelfServiceFlowLoginUI(r.Context()).String() - providerLabel := providerID - provider, _ := s.provider(r.Context(), r, providerID) - if provider != nil && provider.Config() != nil { - providerLabel = provider.Config().Label - } - lf.UI.Messages.Add(text.NewInfoLoginLinkMessage(dc.DuplicateIdentifier, providerLabel, newLoginURL)) - - err := s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), lf) - if err != nil { + redirectURL = lf.AppendTo(returnTo) + } else { + redirectURL = lf.AppendTo(s.d.Config().SelfServiceFlowLoginUI(ctx)) + } + if dc, err := flow.DuplicateCredentials(lf); err == nil && dc != nil { + redirectURL = urlx.CopyWithQuery(redirectURL, url.Values{"no_org_ui": {"true"}}) + s.populateAccountLinkingUI(ctx, lf, usedProviderID, dc.DuplicateIdentifier, dup.AvailableCredentials(), dup.AvailableOIDCProviders()) + if err := s.d.LoginFlowPersister().UpdateLoginFlow(ctx, lf); err != nil { return err } } @@ -637,15 +624,15 @@ func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Fl // Adds the "Continue" button rf.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - AddProvider(rf.UI, providerID, text.NewInfoRegistrationContinue()) + AddProvider(rf.UI, usedProviderID, text.NewInfoRegistrationContinue()) if traits != nil { - ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(ctx) if err != nil { return err } - traitNodes, err := container.NodesFromJSONSchema(r.Context(), node.OpenIDConnectGroup, ds.String(), "", nil) + traitNodes, err := container.NodesFromJSONSchema(ctx, node.OpenIDConnectGroup, ds.String(), "", nil) if err != nil { return err } @@ -662,18 +649,88 @@ func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Fl return err } +func (s *Strategy) populateAccountLinkingUI(ctx context.Context, lf *login.Flow, usedProviderID string, duplicateIdentifier string, availableCredentials []string, availableProviders []string) { + newLoginURL := s.d.Config().SelfServiceFlowLoginUI(ctx).String() + usedProviderLabel := usedProviderID + provider, _ := s.provider(ctx, usedProviderID) + if provider != nil && provider.Config() != nil { + usedProviderLabel = provider.Config().Label + if usedProviderLabel == "" { + usedProviderLabel = provider.Config().Provider + } + } + loginHintsEnabled := s.d.Config().SelfServiceFlowRegistrationLoginHints(ctx) + nodes := []*node.Node{} + for _, n := range lf.UI.Nodes { + // We don't want to touch nodes unecessary nodes + if n.Meta == nil || n.Meta.Label == nil || n.Group == node.DefaultGroup { + nodes = append(nodes, n) + continue + } + + // Skip the provider that was used to get here (in case they used an OIDC provider) + pID := gjson.GetBytes(n.Meta.Label.Context, "provider_id").String() + if n.Group == node.OpenIDConnectGroup { + if pID == usedProviderID { + continue + } + // Hide any provider that is not available for the user + if loginHintsEnabled && !slices.Contains(availableProviders, pID) { + continue + } + } + + // Replace some labels to make it easier for the user to understand what's going on. + switch n.Meta.Label.ID { + case text.InfoSelfServiceLogin: + n.Meta.Label = text.NewInfoLoginAndLink() + case text.InfoSelfServiceLoginWith: + p := gjson.GetBytes(n.Meta.Label.Context, "provider").String() + n.Meta.Label = text.NewInfoLoginWithAndLink(p) + } + + // This can happen, if login hints are disabled. In that case, we need to make sure to show all credential options. + // It could in theory also happen due to a mis-configuration, and in that case, we should make sure to not delete the entire flow. + if !loginHintsEnabled { + nodes = append(nodes, n) + } else { + // Hide nodes from credentials that are not relevant for the user + for _, ct := range availableCredentials { + if ct == string(n.Group) { + nodes = append(nodes, n) + break + } + } + } + } + + // Hide the "primary" identifier field present for Password, webauthn or passwordless, as we already know the identifier + identifierNode := lf.UI.Nodes.Find("identifier") + if identifierNode != nil { + if attributes, ok := identifierNode.Attributes.(*node.InputAttributes); ok { + attributes.Type = node.InputAttributeTypeHidden + attributes.SetValue(duplicateIdentifier) + identifierNode.Attributes = attributes + } + } + + lf.UI.Nodes = nodes + lf.UI.Messages.Clear() + lf.UI.Messages.Add(text.NewInfoLoginLinkMessage(duplicateIdentifier, usedProviderLabel, newLoginURL, availableCredentials, availableProviders)) +} + func (s *Strategy) NodeGroup() node.UiNodeGroup { return node.OpenIDConnectGroup } -func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context, _ session.AuthenticationMethods) session.AuthenticationMethod { +func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.AuthenticationMethod { return session.AuthenticationMethod{ Method: s.ID(), AAL: identity.AuthenticatorAssuranceLevel1, } } -func (s *Strategy) processIDToken(w http.ResponseWriter, r *http.Request, provider Provider, idToken, idTokenNonce string) (*Claims, error) { +func (s *Strategy) processIDToken(r *http.Request, provider Provider, idToken, idTokenNonce string) (*Claims, error) { verifier, ok := provider.(IDTokenVerifier) if !ok { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The provider %s does not support id_token verification", provider.Config().Provider)) @@ -709,7 +766,7 @@ func (s *Strategy) processIDToken(w http.ResponseWriter, r *http.Request, provid return claims, nil } -func (s *Strategy) linkCredentials(ctx context.Context, i *identity.Identity, idToken, accessToken, refreshToken, provider, subject, organization string) error { +func (s *Strategy) linkCredentials(ctx context.Context, i *identity.Identity, tokens *identity.CredentialsOIDCEncryptedTokens, provider, subject, organization string) error { if err := s.d.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, i, identity.ExpandCredentials); err != nil { return err } @@ -717,7 +774,7 @@ func (s *Strategy) linkCredentials(ctx context.Context, i *identity.Identity, id creds, err := i.ParseCredentials(s.ID(), &conf) if errors.Is(err, herodot.ErrNotFound) { var err error - if creds, err = identity.NewCredentialsOIDC(idToken, accessToken, refreshToken, provider, subject, organization); err != nil { + if creds, err = identity.NewCredentialsOIDC(tokens, provider, subject, organization); err != nil { return err } } else if err != nil { @@ -726,9 +783,9 @@ func (s *Strategy) linkCredentials(ctx context.Context, i *identity.Identity, id creds.Identifiers = append(creds.Identifiers, identity.OIDCUniqueID(provider, subject)) conf.Providers = append(conf.Providers, identity.CredentialsOIDCProvider{ Subject: subject, Provider: provider, - InitialAccessToken: accessToken, - InitialRefreshToken: refreshToken, - InitialIDToken: idToken, + InitialAccessToken: tokens.GetAccessToken(), + InitialRefreshToken: tokens.GetRefreshToken(), + InitialIDToken: tokens.GetIDToken(), Organization: organization, }) @@ -745,3 +802,47 @@ func (s *Strategy) linkCredentials(ctx context.Context, i *identity.Identity, id return nil } + +func getAuthRedirectURL(ctx context.Context, provider Provider, req ider, state string, upstreamParameters map[string]string, opts []oauth2.AuthCodeOption) (codeURL string, err error) { + switch p := provider.(type) { + case OAuth2Provider: + c, err := p.OAuth2(ctx) + if err != nil { + return "", err + } + opts = append(opts, UpstreamParameters(upstreamParameters)...) + opts = append(opts, p.AuthCodeURLOptions(req)...) + + return c.AuthCodeURL(state, opts...), nil + case OAuth1Provider: + return p.AuthURL(ctx, state) + default: + return "", errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The provider %s does not support the OAuth 2.0 or OAuth 1.0 protocol", provider.Config().Provider)) + } +} + +func (s *Strategy) encryptOAuth2Tokens(ctx context.Context, token *oauth2.Token) (et *identity.CredentialsOIDCEncryptedTokens, err error) { + et = new(identity.CredentialsOIDCEncryptedTokens) + if token == nil { + return et, nil + } + + if idToken, ok := token.Extra("id_token").(string); ok { + et.IDToken, err = s.d.Cipher(ctx).Encrypt(ctx, []byte(idToken)) + if err != nil { + return nil, err + } + } + + et.AccessToken, err = s.d.Cipher(ctx).Encrypt(ctx, []byte(token.AccessToken)) + if err != nil { + return nil, err + } + + et.RefreshToken, err = s.d.Cipher(ctx).Encrypt(ctx, []byte(token.RefreshToken)) + if err != nil { + return nil, err + } + + return et, nil +} diff --git a/selfservice/strategy/oidc/strategy_helper_test.go b/selfservice/strategy/oidc/strategy_helper_test.go index 7b39729cc6af..2b542cb20e57 100644 --- a/selfservice/strategy/oidc/strategy_helper_test.go +++ b/selfservice/strategy/oidc/strategy_helper_test.go @@ -10,6 +10,7 @@ import ( "encoding/json" "fmt" "io" + "net" "net/http" "net/http/httptest" "net/url" @@ -76,19 +77,43 @@ func (token *idTokenClaims) MarshalJSON() ([]byte, error) { }) } -func createClient(t *testing.T, remote string, redir string) (id, secret string) { +func createClient(t *testing.T, remote string, redir []string) (id, secret string) { require.NoError(t, resilience.Retry(logrusx.New("", ""), time.Second*10, time.Minute*2, func() error { var b bytes.Buffer require.NoError(t, json.NewEncoder(&b).Encode(&struct { - Scope string `json:"scope"` - GrantTypes []string `json:"grant_types"` - ResponseTypes []string `json:"response_types"` - RedirectURIs []string `json:"redirect_uris"` + Scope string `json:"scope"` + GrantTypes []string `json:"grant_types"` + ResponseTypes []string `json:"response_types"` + RedirectURIs []string `json:"redirect_uris"` + TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"` }{ GrantTypes: []string{"authorization_code", "refresh_token"}, ResponseTypes: []string{"code"}, Scope: "offline offline_access openid", - RedirectURIs: []string{redir}, + RedirectURIs: redir, + + // This is a workaround to prevent golang.org/x/oauth2 from + // swallowing the actual error messages from failed token exchanges. + // + // The library first attempts to use the Authorization header to + // pass Client ID+secret during token exchange (client_secret_basic + // in Hydra terminology). If that fails (with any error), it tries + // again with the Client ID+secret passed in the HTTP POST body + // (client_secret_post in Hydra). If that also fails, this second + // error is returned. + // + // Now, if the the client was indeed configured to use + // client_secret_basic, but the token exchange fails for another + // reason, the error message will be swallowed and replaced with + // "invalid_client". + // + // Manually setting this to client_secret_post means that during + // tests, all token exchanges will first fail with `invalid_client` + // and then be retried with the correct method. This is the only way + // to get the actual error message from the server, however. + // + // https://github.com/golang/oauth2/blob/5fd42413edb3b1699004a31b72e485e0e4ba1b13/internal/token.go#L227-L242 + TokenEndpointAuthMethod: "client_secret_post", })) res, err := http.Post(remote+"/admin/clients", "application/json", &b) @@ -179,17 +204,12 @@ func newHydraIntegration(t *testing.T, remote *string, subject *string, claims * parsed, err := url.ParseRequestURI(addr) require.NoError(t, err) - //#nosec G112 - server := &http.Server{Addr: ":" + parsed.Port(), Handler: router} - go func(t *testing.T) { - if err := server.ListenAndServe(); err != http.ErrServerClosed { - require.NoError(t, err) - } else if err == nil { - require.NoError(t, server.Close()) - } - }(t) + listener, err := net.Listen("tcp", ":"+parsed.Port()) + require.NoError(t, err, "port busy?") + server := &http.Server{Handler: router} + go server.Serve(listener) t.Cleanup(func() { - require.NoError(t, server.Close()) + assert.NoError(t, server.Close()) }) return server, addr } @@ -317,7 +337,7 @@ func newOIDCProvider( id string, opts ...func(*oidc.Configuration), ) oidc.Configuration { - clientID, secret := createClient(t, hydraAdmin, kratos.URL+oidc.RouteBase+"/callback/"+id) + clientID, secret := createClient(t, hydraAdmin, []string{kratos.URL + oidc.RouteBase + "/callback/" + id, kratos.URL + oidc.RouteCallbackGeneric}) cfg := oidc.Configuration{ Provider: "generic", diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index be3c6762e3e3..4805919f80d1 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -5,51 +5,41 @@ package oidc import ( "bytes" + "context" "encoding/json" "net/http" "strings" "time" - "github.com/julienschmidt/httprouter" - "golang.org/x/oauth2" - - "github.com/ory/kratos/session" - - "github.com/ory/kratos/ui/node" - "github.com/ory/x/otelx" - "github.com/ory/x/sqlcon" - - "github.com/ory/kratos/selfservice/flow/registration" - - "github.com/ory/kratos/text" - - "github.com/ory/kratos/continuity" - "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" "github.com/ory/herodot" - + "github.com/ory/kratos/continuity" "github.com/ory/kratos/identity" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/flowhelpers" + "github.com/ory/kratos/selfservice/strategy/idfirst" + "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/stringsx" ) -var _ login.Strategy = new(Strategy) +var ( + _ login.FormHydrator = new(Strategy) + _ login.Strategy = new(Strategy) +) func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) { s.setRoutes(r) } -func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, l *login.Flow) error { - // This strategy can only solve AAL1 - if requestedAAL > identity.AuthenticatorAssuranceLevel1 { - return nil - } - - return s.populateMethod(r, l, text.NewInfoLoginWith) -} - // Update Login Flow with OpenID Connect Method // // swagger:model updateLoginFlowWithOidcMethod @@ -92,6 +82,7 @@ type UpdateLoginFlowWithOidcMethod struct { // // Supported providers are // - Apple + // - Google // required: false IDToken string `json:"id_token,omitempty"` @@ -100,10 +91,18 @@ type UpdateLoginFlowWithOidcMethod struct { // // required: false IDTokenNonce string `json:"id_token_nonce,omitempty"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } -func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, loginFlow *login.Flow, token *oauth2.Token, claims *Claims, provider Provider, container *AuthCodeContainer) (*registration.Flow, error) { - i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), identity.CredentialsTypeOIDC, identity.OIDCUniqueID(provider.Config().ID, claims.Subject)) +func (s *Strategy) processLogin(ctx context.Context, w http.ResponseWriter, r *http.Request, loginFlow *login.Flow, token *identity.CredentialsOIDCEncryptedTokens, claims *Claims, provider Provider, container *AuthCodeContainer) (_ *registration.Flow, err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.strategy.processLogin") + defer otelx.End(span, &err) + + i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, identity.CredentialsTypeOIDC, identity.OIDCUniqueID(provider.Config().ID, claims.Subject)) if err != nil { if errors.Is(err, sqlcon.ErrNoRows) { // If no account was found we're "manually" creating a new registration flow and redirecting the browser @@ -134,70 +133,75 @@ func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, loginFlo registrationFlow, err := s.d.RegistrationHandler().NewRegistrationFlow(w, r, loginFlow.Type, opts...) if err != nil { - return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, err) + return nil, s.handleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err) } - err = s.d.SessionTokenExchangePersister().MoveToNewFlow(r.Context(), loginFlow.ID, registrationFlow.ID) + err = s.d.SessionTokenExchangePersister().MoveToNewFlow(ctx, loginFlow.ID, registrationFlow.ID) if err != nil { - return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, err) + return nil, s.handleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err) } registrationFlow.OrganizationID = loginFlow.OrganizationID registrationFlow.IDToken = loginFlow.IDToken registrationFlow.RawIDTokenNonce = loginFlow.RawIDTokenNonce registrationFlow.RequestURL, err = x.TakeOverReturnToParameter(loginFlow.RequestURL, registrationFlow.RequestURL) + registrationFlow.TransientPayload = loginFlow.TransientPayload + registrationFlow.Active = s.ID() + if err != nil { - return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, err) + return nil, s.handleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err) } - if _, err := s.processRegistration(w, r, registrationFlow, token, claims, provider, container, loginFlow.IDToken); err != nil { + if _, err := s.processRegistration(ctx, w, r, registrationFlow, token, claims, provider, container); err != nil { return registrationFlow, err } return nil, nil } - return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, err) + return nil, s.handleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err) } var oidcCredentials identity.CredentialsOIDC if err := json.NewDecoder(bytes.NewBuffer(c.Config)).Decode(&oidcCredentials); err != nil { - return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("The password credentials could not be decoded properly").WithDebug(err.Error()))) + return nil, s.handleError(ctx, w, r, loginFlow, provider.Config().ID, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("The password credentials could not be decoded properly").WithDebug(err.Error()))) } sess := session.NewInactiveSession() - sess.CompletedLoginForWithProvider(s.ID(), identity.AuthenticatorAssuranceLevel1, provider.Config().ID, - httprouter.ParamsFromContext(r.Context()).ByName("organization")) + sess.CompletedLoginForWithProvider(s.ID(), identity.AuthenticatorAssuranceLevel1, provider.Config().ID, provider.Config().OrganizationID) for _, c := range oidcCredentials.Providers { if c.Subject == claims.Subject && c.Provider == provider.Config().ID { if err = s.d.LoginHookExecutor().PostLoginHook(w, r, node.OpenIDConnectGroup, loginFlow, i, sess, provider.Config().ID); err != nil { - return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, err) + return nil, s.handleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err) } return nil, nil } } - return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to find matching OpenID Connect Credentials.").WithDebugf(`Unable to find credentials that match the given provider "%s" and subject "%s".`, provider.Config().ID, claims.Subject))) + return nil, s.handleError(ctx, w, r, loginFlow, provider.Config().ID, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to find matching OpenID Connect Credentials.").WithDebugf(`Unable to find credentials that match the given provider "%s" and subject "%s".`, provider.Config().ID, claims.Subject))) } func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, _ *session.Session) (i *identity.Identity, err error) { - ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.oidc.strategy.Login") + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.oidc.Strategy.Login") defer otelx.End(span, &err) if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil { + span.SetAttributes(attribute.String("not_responsible_reason", "requested AAL is not AAL1")) return nil, err } var p UpdateLoginFlowWithOidcMethod - if err := s.newLinkDecoder(&p, r); err != nil { - return nil, s.handleError(w, r, f, "", nil, err) + if err := s.newLinkDecoder(ctx, &p, r); err != nil { + return nil, s.handleError(ctx, w, r, f, "", nil, err) } f.IDToken = p.IDToken f.RawIDTokenNonce = p.IDTokenNonce + f.TransientPayload = p.TransientPayload pid := p.Provider // this can come from both url query and post body if pid == "" { + span.SetAttributes(attribute.String("not_responsible_reason", "provider ID missing")) return nil, errors.WithStack(flow.ErrStrategyNotResponsible) } @@ -208,66 +212,63 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, WithField("provider", p.Provider). WithField("method", p.Method). Warn("The payload includes a `provider` field but is using a method other than `oidc`. Therefore, social sign in will not be executed.") + span.SetAttributes(attribute.String("not_responsible_reason", "method is not oidc")) return nil, errors.WithStack(flow.ErrStrategyNotResponsible) } if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { - return nil, s.handleError(w, r, f, pid, nil, err) - } - - provider, err := s.provider(ctx, r, pid) - if err != nil { - return nil, s.handleError(w, r, f, pid, nil, err) + return nil, s.handleError(ctx, w, r, f, pid, nil, err) } - c, err := provider.OAuth2(ctx) + provider, err := s.provider(ctx, pid) if err != nil { - return nil, s.handleError(w, r, f, pid, nil, err) + return nil, s.handleError(ctx, w, r, f, pid, nil, err) } req, err := s.validateFlow(ctx, r, f.ID) if err != nil { - return nil, s.handleError(w, r, f, pid, nil, err) + return nil, s.handleError(ctx, w, r, f, pid, nil, err) } - if authenticated, err := s.alreadyAuthenticated(w, r, req); err != nil { - return nil, s.handleError(w, r, f, pid, nil, err) + if authenticated, err := s.alreadyAuthenticated(ctx, w, r, req); err != nil { + return nil, s.handleError(ctx, w, r, f, pid, nil, err) } else if authenticated { return i, nil } if p.IDToken != "" { - claims, err := s.processIDToken(w, r, provider, p.IDToken, p.IDTokenNonce) + claims, err := s.processIDToken(r, provider, p.IDToken, p.IDTokenNonce) if err != nil { - return nil, s.handleError(w, r, f, pid, nil, err) + return nil, s.handleError(ctx, w, r, f, pid, nil, err) } - _, err = s.processLogin(w, r, f, nil, claims, provider, &AuthCodeContainer{ + _, err = s.processLogin(ctx, w, r, f, nil, claims, provider, &AuthCodeContainer{ FlowID: f.ID.String(), Traits: p.Traits, }) if err != nil { - return nil, s.handleError(w, r, f, pid, nil, err) + return nil, s.handleError(ctx, w, r, f, pid, nil, err) } return nil, errors.WithStack(flow.ErrCompletedByStrategy) } - state := generateState(f.ID.String()) - if code, hasCode, _ := s.d.SessionTokenExchangePersister().CodeForFlow(ctx, f.ID); hasCode { - state.setCode(code.InitCode) + state, pkce, err := s.GenerateState(ctx, provider, f.ID) + if err != nil { + return nil, s.handleError(ctx, w, r, f, pid, nil, err) } if err := s.d.ContinuityManager().Pause(ctx, w, r, sessionName, continuity.WithPayload(&AuthCodeContainer{ - State: state.String(), - FlowID: f.ID.String(), - Traits: p.Traits, + State: state, + FlowID: f.ID.String(), + Traits: p.Traits, + TransientPayload: f.TransientPayload, }), continuity.WithLifespan(time.Minute*30)); err != nil { - return nil, s.handleError(w, r, f, pid, nil, err) + return nil, s.handleError(ctx, w, r, f, pid, nil, err) } f.Active = s.ID() if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { - return nil, s.handleError(w, r, f, pid, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error()))) + return nil, s.handleError(ctx, w, r, f, pid, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error()))) } var up map[string]string @@ -275,7 +276,11 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, err } - codeURL := c.AuthCodeURL(state.String(), append(UpstreamParameters(provider, up), provider.AuthCodeURLOptions(req)...)...) + codeURL, err := getAuthRedirectURL(ctx, provider, f, state, up, pkce) + if err != nil { + return nil, s.handleError(ctx, w, r, f, pid, nil, err) + } + if x.IsJSONRequest(r) { s.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(codeURL)) } else { @@ -284,3 +289,100 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, errors.WithStack(flow.ErrCompletedByStrategy) } + +func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, lf *login.Flow) error { + conf, err := s.Config(r.Context()) + if err != nil { + return err + } + + var providers []Configuration + _, id, c := flowhelpers.GuessForcedLoginIdentifier(r, s.d, lf, s.ID()) + if id == nil || c == nil { + providers = nil + } else { + var credentials identity.CredentialsOIDC + if err := json.Unmarshal(c.Config, &credentials); err != nil { + // failed to read OIDC credentials, don't add any providers + providers = nil + } else { + // add only providers that can actually be used to log in as this identity + providers = make([]Configuration, 0, len(conf.Providers)) + for i := range conf.Providers { + for j := range credentials.Providers { + if conf.Providers[i].ID == credentials.Providers[j].Provider { + providers = append(providers, conf.Providers[i]) + break + } + } + } + } + } + + lf.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + AddProviders(lf.UI, providers, text.NewInfoLoginWith) + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, f *login.Flow) error { + return s.populateMethod(r, f, text.NewInfoLoginWith) +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, f *login.Flow, mods ...login.FormHydratorModifier) (err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.oidc.strategy.PopulateLoginMethodIdentifierFirstCredentials") + defer otelx.End(span, &err) + + conf, err := s.Config(ctx) + if err != nil { + return err + } + + o := login.NewFormHydratorOptions(mods) + + var linked []Provider + if o.IdentityHint != nil { + var err error + // If we have an identity hint we check if the identity has any providers configured. + if linked, err = s.linkedProviders(conf, o.IdentityHint); err != nil { + return err + } + } + + if len(linked) == 0 { + // If we found no credentials: + if s.d.Config().SecurityAccountEnumerationMitigate(ctx) { + // We found no credentials but do not want to leak that we know that. So we return early and do not + // modify the initial provider list. + return nil + } + + // We found no credentials. We remove all the providers and tell the strategy that we found nothing. + f.GetUI().UnsetNode("provider") + return idfirst.ErrNoCredentialsFound + } + + if !s.d.Config().SecurityAccountEnumerationMitigate(ctx) { + // Account enumeration is disabled, so we show all providers that are linked to the identity. + // User is found and enumeration mitigation is disabled. Filter the list! + f.GetUI().UnsetNode("provider") + + for _, l := range linked { + lc := l.Config() + AddProvider(f.UI, lc.ID, text.NewInfoLoginWith(stringsx.Coalesce(lc.Label, lc.ID), lc.ID)) + } + } + + return nil +} + +func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, f *login.Flow) error { + return s.populateMethod(r, f, text.NewInfoLoginWith) +} diff --git a/selfservice/strategy/oidc/strategy_login_test.go b/selfservice/strategy/oidc/strategy_login_test.go new file mode 100644 index 000000000000..2191d5cf59a7 --- /dev/null +++ b/selfservice/strategy/oidc/strategy_login_test.go @@ -0,0 +1,166 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/ory/kratos/selfservice/strategy/idfirst" + + configtesthelpers "github.com/ory/kratos/driver/config/testhelpers" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/x" + "github.com/ory/x/snapshotx" +) + +func createIdentity(t *testing.T, ctx context.Context, reg driver.Registry, id uuid.UUID, provider string) *identity.Identity { + creds, err := identity.NewCredentialsOIDC(new(identity.CredentialsOIDCEncryptedTokens), provider, id.String(), "") + require.NoError(t, err) + + i := identity.NewIdentity("default") + i.SetCredentials(identity.CredentialsTypeOIDC, *creds) + + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + return i +} + +func TestFormHydration(t *testing.T) { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + providerID := "test-provider" + + ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".enabled", true) + ctx = configtesthelpers.WithConfigValue( + ctx, + config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".config", + map[string]interface{}{ + "providers": []map[string]interface{}{ + { + "provider": "generic", + "id": providerID, + "client_id": "invalid", + "client_secret": "invalid", + "issuer_url": "https://foobar/", + "mapper_url": "file://./stub/oidc.facebook.jsonnet", + }, + }, + }, + ) + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/stub.schema.json") + + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeOIDC) + require.NoError(t, err) + fh, ok := s.(login.FormHydrator) + require.True(t, ok) + + toSnapshot := func(t *testing.T, f *login.Flow) { + t.Helper() + // The CSRF token has a unique value that messes with the snapshot - ignore it. + f.UI.Nodes.ResetNodes("csrf_token") + snapshotx.SnapshotT(t, f.UI.Nodes) + } + newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) { + r := httptest.NewRequest("GET", "/self-service/login/browser", nil) + r = r.WithContext(ctx) + t.Helper() + f, err := login.NewFlow(conf, time.Minute, "csrf_token", r, flow.TypeBrowser) + require.NoError(t, err) + return r, f + } + + t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) { + r, f := newFlow(ctx, t) + f.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodFirstFactorRefresh", func(t *testing.T) { + r, f := newFlow(ctx, t) + + id := createIdentity(t, ctx, reg, x.NewUUID(), providerID) + r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + f.Refresh = true + + require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodSecondFactorRefresh", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) { + t.Run("case=no options", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=WithIdentifier", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=WithIdentityHint", func(t *testing.T) { + t.Run("case=account enumeration mitigation enabled", func(t *testing.T) { + ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true) + + id := identity.NewIdentity(providerID) + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id))) + toSnapshot(t, f) + }) + + t.Run("case=account enumeration mitigation disabled", func(t *testing.T) { + ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false) + + t.Run("case=identity has oidc", func(t *testing.T) { + identifier := x.NewUUID() + id := createIdentity(t, ctx, reg, identifier, providerID) + + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id))) + toSnapshot(t, f) + }) + + t.Run("case=identity does not have a oidc", func(t *testing.T) { + id := identity.NewIdentity("default") + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + }) + }) + }) + + t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f)) + toSnapshot(t, f) + }) +} diff --git a/selfservice/strategy/oidc/strategy_registration.go b/selfservice/strategy/oidc/strategy_registration.go index 84aba3986563..f7740a27b9b9 100644 --- a/selfservice/strategy/oidc/strategy_registration.go +++ b/selfservice/strategy/oidc/strategy_registration.go @@ -5,18 +5,19 @@ package oidc import ( "bytes" + "context" "encoding/json" "net/http" "strings" "time" + "go.opentelemetry.io/otel/attribute" + "github.com/dgraph-io/ristretto" "github.com/gofrs/uuid" - "github.com/julienschmidt/httprouter" "github.com/pkg/errors" "github.com/tidwall/gjson" "github.com/tidwall/sjson" - "golang.org/x/oauth2" "github.com/ory/herodot" "github.com/ory/kratos/continuity" @@ -84,11 +85,6 @@ type UpdateRegistrationFlowWithOidcMethod struct { // required: true Method string `json:"method"` - // Transient data to pass along to any webhooks - // - // required: false - TransientPayload json.RawMessage `json:"transient_payload,omitempty"` - // UpstreamParameters are the parameters that are passed to the upstream identity provider. // // These parameters are optional and depend on what the upstream identity provider supports. @@ -109,6 +105,7 @@ type UpdateRegistrationFlowWithOidcMethod struct { // // Supported providers are // - Apple + // - Google // required: false IDToken string `json:"id_token,omitempty"` @@ -117,10 +114,15 @@ type UpdateRegistrationFlowWithOidcMethod struct { // // required: false IDTokenNonce string `json:"id_token_nonce,omitempty"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } -func (s *Strategy) newLinkDecoder(p interface{}, r *http.Request) error { - ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) +func (s *Strategy) newLinkDecoder(ctx context.Context, p interface{}, r *http.Request) error { + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(ctx) if err != nil { return err } @@ -148,24 +150,25 @@ func (s *Strategy) newLinkDecoder(p interface{}, r *http.Request) error { return nil } -func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registration.Flow, i *identity.Identity) (err error) { - ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.oidc.strategy.Register") +func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registration.Flow, _ *identity.Identity) (err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.oidc.Strategy.Register") defer otelx.End(span, &err) var p UpdateRegistrationFlowWithOidcMethod - if err := s.newLinkDecoder(&p, r); err != nil { - return s.handleError(w, r, f, "", nil, err) + if err := s.newLinkDecoder(ctx, &p, r); err != nil { + return s.handleError(ctx, w, r, f, "", nil, err) } - f.TransientPayload = p.TransientPayload - f.IDToken = p.IDToken - f.RawIDTokenNonce = p.IDTokenNonce - pid := p.Provider // this can come from both url query and post body if pid == "" { + span.SetAttributes(attribute.String("not_responsible_reason", "provider ID missing")) return errors.WithStack(flow.ErrStrategyNotResponsible) } + f.TransientPayload = p.TransientPayload + f.IDToken = p.IDToken + f.RawIDTokenNonce = p.IDTokenNonce + if !strings.EqualFold(strings.ToLower(p.Method), s.SettingsStrategyID()) && p.Method != "" { // the user is sending a method that is not oidc, but the payload includes a provider s.d.Audit(). @@ -173,51 +176,50 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat WithField("provider", p.Provider). WithField("method", p.Method). Warn("The payload includes a `provider` field but is using a method other than `oidc`. Therefore, social sign in will not be executed.") + span.SetAttributes(attribute.String("not_responsible_reason", "method is not oidc")) return errors.WithStack(flow.ErrStrategyNotResponsible) } if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { - return s.handleError(w, r, f, pid, nil, err) - } - - provider, err := s.provider(ctx, r, pid) - if err != nil { - return s.handleError(w, r, f, pid, nil, err) + return s.handleError(ctx, w, r, f, pid, nil, err) } - c, err := provider.OAuth2(ctx) + provider, err := s.provider(ctx, pid) if err != nil { - return s.handleError(w, r, f, pid, nil, err) + return s.handleError(ctx, w, r, f, pid, nil, err) } req, err := s.validateFlow(ctx, r, f.ID) if err != nil { - return s.handleError(w, r, f, pid, nil, err) + return s.handleError(ctx, w, r, f, pid, nil, err) } - if authenticated, err := s.alreadyAuthenticated(w, r, req); err != nil { - return s.handleError(w, r, f, pid, nil, err) + if authenticated, err := s.alreadyAuthenticated(ctx, w, r, req); err != nil { + return s.handleError(ctx, w, r, f, pid, nil, err) } else if authenticated { return errors.WithStack(registration.ErrAlreadyLoggedIn) } if p.IDToken != "" { - claims, err := s.processIDToken(w, r, provider, p.IDToken, p.IDTokenNonce) + claims, err := s.processIDToken(r, provider, p.IDToken, p.IDTokenNonce) if err != nil { - return s.handleError(w, r, f, pid, nil, err) + return s.handleError(ctx, w, r, f, pid, nil, err) } - _, err = s.processRegistration(w, r, f, nil, claims, provider, &AuthCodeContainer{ + _, err = s.processRegistration(ctx, w, r, f, nil, claims, provider, &AuthCodeContainer{ FlowID: f.ID.String(), Traits: p.Traits, TransientPayload: f.TransientPayload, - }, p.IDToken) + }) if err != nil { - return s.handleError(w, r, f, pid, nil, err) + return s.handleError(ctx, w, r, f, pid, nil, err) } return errors.WithStack(flow.ErrCompletedByStrategy) } - state := generateState(f.ID.String()) + state, pkce, err := s.GenerateState(ctx, provider, f.ID) + if err != nil { + return s.handleError(ctx, w, r, f, pid, nil, err) + } // fandom-start extraFields := map[string]string{} _ = r.ParseForm() @@ -228,13 +230,9 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat extraFields[k] = v[0] } // fandom-end - - if code, hasCode, _ := s.d.SessionTokenExchangePersister().CodeForFlow(r.Context(), f.ID); hasCode { - state.setCode(code.InitCode) - } if err := s.d.ContinuityManager().Pause(ctx, w, r, sessionName, continuity.WithPayload(&AuthCodeContainer{ - State: state.String(), + State: state, FlowID: f.ID.String(), Traits: p.Traits, TransientPayload: f.TransientPayload, @@ -243,7 +241,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat // fandom-end }), continuity.WithLifespan(time.Minute*30)); err != nil { - return s.handleError(w, r, f, pid, nil, err) + return s.handleError(ctx, w, r, f, pid, nil, err) } var up map[string]string @@ -251,7 +249,10 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat return err } - codeURL := c.AuthCodeURL(state.String(), append(UpstreamParameters(provider, up), provider.AuthCodeURLOptions(req)...)...) + codeURL, err := getAuthRedirectURL(ctx, provider, f, state, up, pkce) + if err != nil { + return s.handleError(ctx, w, r, f, pid, nil, err) + } if x.IsJSONRequest(r) { s.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(codeURL)) } else { @@ -261,7 +262,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat return errors.WithStack(flow.ErrCompletedByStrategy) } -func (s *Strategy) registrationToLogin(w http.ResponseWriter, r *http.Request, rf *registration.Flow, providerID string) (*login.Flow, error) { +func (s *Strategy) registrationToLogin(ctx context.Context, w http.ResponseWriter, r *http.Request, rf *registration.Flow) (*login.Flow, error) { // If return_to was set before, we need to preserve it. var opts []login.FlowOption if len(rf.ReturnTo) > 0 { @@ -272,14 +273,14 @@ func (s *Strategy) registrationToLogin(w http.ResponseWriter, r *http.Request, r opts = append(opts, login.WithFormErrorMessage(rf.UI.Messages)) } - opts = append(opts, login.WithInternalContext(rf.InternalContext)) + opts = append(opts, login.WithInternalContext(rf.InternalContext), login.WithIsAccountLinking()) lf, _, err := s.d.LoginHandler().NewLoginFlow(w, r, rf.Type, opts...) if err != nil { return nil, err } - err = s.d.SessionTokenExchangePersister().MoveToNewFlow(r.Context(), rf.ID, lf.ID) + err = s.d.SessionTokenExchangePersister().MoveToNewFlow(ctx, rf.ID, lf.ID) if err != nil { return nil, err } @@ -288,12 +289,17 @@ func (s *Strategy) registrationToLogin(w http.ResponseWriter, r *http.Request, r if err != nil { return nil, err } + lf.TransientPayload = rf.TransientPayload + lf.Active = s.ID() return lf, nil } -func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, rf *registration.Flow, token *oauth2.Token, claims *Claims, provider Provider, container *AuthCodeContainer, idToken string) (*login.Flow, error) { - if _, _, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), identity.CredentialsTypeOIDC, identity.OIDCUniqueID(provider.Config().ID, claims.Subject)); err == nil { +func (s *Strategy) processRegistration(ctx context.Context, w http.ResponseWriter, r *http.Request, rf *registration.Flow, token *identity.CredentialsOIDCEncryptedTokens, claims *Claims, provider Provider, container *AuthCodeContainer) (_ *login.Flow, err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.strategy.processRegistration") + defer otelx.End(span, &err) + + if _, _, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, identity.CredentialsTypeOIDC, identity.OIDCUniqueID(provider.Config().ID, claims.Subject)); err == nil { // If the identity already exists, we should perform the login flow instead. // That will execute the "pre registration" hook which allows to e.g. disallow this flow. The registration @@ -307,32 +313,32 @@ func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, r WithField("subject", claims.Subject). Debug("Received successful OpenID Connect callback but user is already registered. Re-initializing login flow now.") - lf, err := s.registrationToLogin(w, r, rf, provider.Config().ID) + lf, err := s.registrationToLogin(ctx, w, r, rf) if err != nil { - return nil, s.handleError(w, r, rf, provider.Config().ID, nil, err) + return nil, s.handleError(ctx, w, r, rf, provider.Config().ID, nil, err) } - if _, err := s.processLogin(w, r, lf, token, claims, provider, container); err != nil { - return lf, s.handleError(w, r, rf, provider.Config().ID, nil, err) + if _, err := s.processLogin(ctx, w, r, lf, token, claims, provider, container); err != nil { + return lf, s.handleError(ctx, w, r, rf, provider.Config().ID, nil, err) } return nil, nil } - fetch := fetcher.NewFetcher(fetcher.WithClient(s.d.HTTPClient(r.Context())), fetcher.WithCache(jsonnetCache, 60*time.Minute)) - jsonnetMapperSnippet, err := fetch.FetchContext(r.Context(), provider.Config().Mapper) + fetch := fetcher.NewFetcher(fetcher.WithClient(s.d.HTTPClient(ctx)), fetcher.WithCache(jsonnetCache, 60*time.Minute)) + jsonnetMapperSnippet, err := fetch.FetchContext(ctx, provider.Config().Mapper) if err != nil { - return nil, s.handleError(w, r, rf, provider.Config().ID, nil, err) + return nil, s.handleError(ctx, w, r, rf, provider.Config().ID, nil, err) } - i, va, err := s.createIdentity(w, r, rf, claims, provider, container, jsonnetMapperSnippet.Bytes()) + i, va, err := s.createIdentity(ctx, w, r, rf, claims, provider, container, jsonnetMapperSnippet.Bytes()) if err != nil { - return nil, s.handleError(w, r, rf, provider.Config().ID, nil, err) + return nil, s.handleError(ctx, w, r, rf, provider.Config().ID, nil, err) } // Validate the identity itself - if err := s.d.IdentityValidator().Validate(r.Context(), i); err != nil { - return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) + if err := s.d.IdentityValidator().Validate(ctx, i); err != nil { + return nil, s.handleError(ctx, w, r, rf, provider.Config().ID, i.Traits, err) } for n := range i.VerifiableAddresses { @@ -347,29 +353,9 @@ func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, r } } - var it string = idToken - var cat, crt string - if token != nil { - if idToken, ok := token.Extra("id_token").(string); ok { - if it, err = s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(idToken)); err != nil { - return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) - } - } - - cat, err = s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(token.AccessToken)) - if err != nil { - return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) - } - - crt, err = s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(token.RefreshToken)) - if err != nil { - return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) - } - } - - creds, err := identity.NewCredentialsOIDC(it, cat, crt, provider.Config().ID, claims.Subject, provider.Config().OrganizationID) + creds, err := identity.NewCredentialsOIDC(token, provider.Config().ID, claims.Subject, provider.Config().OrganizationID) if err != nil { - return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) + return nil, s.handleError(ctx, w, r, rf, provider.Config().ID, i.Traits, err) } // fandom-start @@ -389,50 +375,50 @@ func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, r // fandom-end i.SetCredentials(s.ID(), *creds) - if err := s.d.RegistrationExecutor().PostRegistrationHook(w, r, identity.CredentialsTypeOIDC, provider.Config().ID, rf, i); err != nil { - return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) + if err := s.d.RegistrationExecutor().PostRegistrationHook(w, r, identity.CredentialsTypeOIDC, provider.Config().ID, provider.Config().OrganizationID, rf, i); err != nil { + return nil, s.handleError(ctx, w, r, rf, provider.Config().ID, i.Traits, err) } return nil, nil } -func (s *Strategy) createIdentity(w http.ResponseWriter, r *http.Request, a *registration.Flow, claims *Claims, provider Provider, container *AuthCodeContainer, jsonnetSnippet []byte) (*identity.Identity, []VerifiedAddress, error) { +func (s *Strategy) createIdentity(ctx context.Context, w http.ResponseWriter, r *http.Request, a *registration.Flow, claims *Claims, provider Provider, container *AuthCodeContainer, jsonnetSnippet []byte) (*identity.Identity, []VerifiedAddress, error) { var jsonClaims bytes.Buffer if err := json.NewEncoder(&jsonClaims).Encode(claims); err != nil { - return nil, nil, s.handleError(w, r, a, provider.Config().ID, nil, err) + return nil, nil, s.handleError(ctx, w, r, a, provider.Config().ID, nil, err) } - vm, err := s.d.JsonnetVM(r.Context()) + vm, err := s.d.JsonnetVM(ctx) if err != nil { - return nil, nil, s.handleError(w, r, a, provider.Config().ID, nil, err) + return nil, nil, s.handleError(ctx, w, r, a, provider.Config().ID, nil, err) } vm.ExtCode("claims", jsonClaims.String()) evaluated, err := vm.EvaluateAnonymousSnippet(provider.Config().Mapper, string(jsonnetSnippet)) if err != nil { - return nil, nil, s.handleError(w, r, a, provider.Config().ID, nil, err) + return nil, nil, s.handleError(ctx, w, r, a, provider.Config().ID, nil, err) } - i := identity.NewIdentity(s.d.Config().DefaultIdentityTraitsSchemaID(r.Context())) - if err := s.setTraits(w, r, a, claims, provider, container, evaluated, i); err != nil { - return nil, nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err) + i := identity.NewIdentity(s.d.Config().DefaultIdentityTraitsSchemaID(ctx)) + if err := s.setTraits(ctx, w, r, a, provider, container, evaluated, i); err != nil { + return nil, nil, s.handleError(ctx, w, r, a, provider.Config().ID, i.Traits, err) } if err := s.setMetadata(evaluated, i, PublicMetadata); err != nil { - return nil, nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err) + return nil, nil, s.handleError(ctx, w, r, a, provider.Config().ID, i.Traits, err) } if err := s.setMetadata(evaluated, i, AdminMetadata); err != nil { - return nil, nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err) + return nil, nil, s.handleError(ctx, w, r, a, provider.Config().ID, i.Traits, err) } va, err := s.extractVerifiedAddresses(evaluated) if err != nil { - return nil, nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err) + return nil, nil, s.handleError(ctx, w, r, a, provider.Config().ID, i.Traits, err) } - if orgID := httprouter.ParamsFromContext(r.Context()).ByName("organization"); orgID != "" { - i.OrganizationID = uuid.NullUUID{UUID: x.ParseUUID(orgID), Valid: true} + if orgID, err := uuid.FromString(provider.Config().OrganizationID); err == nil { + i.OrganizationID = uuid.NullUUID{UUID: orgID, Valid: true} } s.d.Logger(). @@ -445,7 +431,7 @@ func (s *Strategy) createIdentity(w http.ResponseWriter, r *http.Request, a *reg return i, va, nil } -func (s *Strategy) setTraits(w http.ResponseWriter, r *http.Request, a *registration.Flow, claims *Claims, provider Provider, container *AuthCodeContainer, evaluated string, i *identity.Identity) error { +func (s *Strategy) setTraits(ctx context.Context, w http.ResponseWriter, r *http.Request, a *registration.Flow, provider Provider, container *AuthCodeContainer, evaluated string, i *identity.Identity) error { jsonTraits := gjson.Get(evaluated, "identity.traits") if !jsonTraits.IsObject() { return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("OpenID Connect Jsonnet mapper did not return an object for key identity.traits. Please check your Jsonnet code!")) @@ -454,12 +440,12 @@ func (s *Strategy) setTraits(w http.ResponseWriter, r *http.Request, a *registra if container != nil { traits, err := merge(container.Traits, json.RawMessage(jsonTraits.Raw)) if err != nil { - return s.handleError(w, r, a, provider.Config().ID, nil, err) + return s.handleError(ctx, w, r, a, provider.Config().ID, nil, err) } i.Traits = traits } else { - i.Traits = identity.Traits(json.RawMessage(jsonTraits.Raw)) + i.Traits = identity.Traits(jsonTraits.Raw) } s.d.Logger(). WithRequest(r). diff --git a/selfservice/strategy/oidc/strategy_settings.go b/selfservice/strategy/oidc/strategy_settings.go index 24623938fd87..9a97a54e5521 100644 --- a/selfservice/strategy/oidc/strategy_settings.go +++ b/selfservice/strategy/oidc/strategy_settings.go @@ -11,40 +11,44 @@ import ( "net/http" "time" - "github.com/ory/x/sqlxx" - - "github.com/tidwall/sjson" - - "golang.org/x/oauth2" - - "github.com/ory/kratos/continuity" - "github.com/ory/kratos/selfservice/strategy" - "github.com/ory/x/decoderx" - - "github.com/ory/kratos/session" - "github.com/gofrs/uuid" "github.com/pkg/errors" + "github.com/tidwall/sjson" + "go.opentelemetry.io/otel/attribute" "github.com/ory/herodot" "github.com/ory/jsonschema/v3" + "github.com/ory/x/decoderx" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlxx" + "github.com/ory/x/stringsx" + + "github.com/ory/kratos/continuity" "github.com/ory/kratos/identity" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/settings" - + "github.com/ory/kratos/selfservice/strategy" + "github.com/ory/kratos/session" "github.com/ory/kratos/x" ) //go:embed .schema/settings.schema.json var settingsSchema []byte -var _ settings.Strategy = new(Strategy) -var UnknownConnectionValidationError = &jsonschema.ValidationError{ - Message: "can not unlink non-existing OpenID Connect connection", InstancePtr: "#/"} +var ( + _ settings.Strategy = new(Strategy) + UnknownConnectionValidationError = &jsonschema.ValidationError{ + Message: "can not unlink non-existing OpenID Connect connection", InstancePtr: "#/", + } +) + var ConnectionExistValidationError = &jsonschema.ValidationError{ - Message: "can not link unknown or already existing OpenID Connect connection", InstancePtr: "#/"} + Message: "can not link unknown or already existing OpenID Connect connection", InstancePtr: "#/", +} + var UnlinkAllFirstFactorConnectionsError = &jsonschema.ValidationError{ - Message: "can not unlink OpenID Connect connection because it is the last remaining first factor credential", InstancePtr: "#/"} + Message: "can not unlink OpenID Connect connection because it is the last remaining first factor credential", InstancePtr: "#/", +} func (s *Strategy) RegisterSettingsRoutes(router *x.RouterPublic) {} @@ -80,7 +84,7 @@ func (s *Strategy) decoderSettings(p *updateSettingsFlowWithOidcMethod, r *http. return nil } -func (s *Strategy) linkedProviders(ctx context.Context, r *http.Request, conf *ConfigurationCollection, confidential *identity.Identity) ([]Provider, error) { +func (s *Strategy) linkedProviders(conf *ConfigurationCollection, confidential *identity.Identity) ([]Provider, error) { creds, ok := confidential.GetCredentials(s.ID()) if !ok { return nil, nil @@ -105,7 +109,7 @@ func (s *Strategy) linkedProviders(ctx context.Context, r *http.Request, conf *C return result, nil } -func (s *Strategy) linkableProviders(ctx context.Context, r *http.Request, conf *ConfigurationCollection, confidential *identity.Identity) ([]Provider, error) { +func (s *Strategy) linkableProviders(conf *ConfigurationCollection, confidential *identity.Identity) ([]Provider, error) { var available identity.CredentialsOIDC creds, ok := confidential.GetCredentials(s.ID()) if ok { @@ -137,26 +141,28 @@ func (s *Strategy) linkableProviders(ctx context.Context, r *http.Request, conf } func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity, sr *settings.Flow) error { + ctx := r.Context() + if sr.Type != flow.TypeBrowser { return nil } - conf, err := s.Config(r.Context()) + conf, err := s.Config(ctx) if err != nil { return err } - confidential, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), id.ID) + confidential, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, id.ID) if err != nil { return err } - linkable, err := s.linkableProviders(r.Context(), r, conf, confidential) + linkable, err := s.linkableProviders(conf, confidential) if err != nil { return err } - linked, err := s.linkedProviders(r.Context(), r, conf, confidential) + linked, err := s.linkedProviders(conf, confidential) if err != nil { return err } @@ -168,10 +174,10 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity if l.Config().OrganizationID != "" { continue } - sr.UI.GetNodes().Append(NewLinkNode(l.Config().ID)) + sr.UI.GetNodes().Append(NewLinkNode(l.Config().ID, stringsx.Coalesce(l.Config().Label, l.Config().ID))) } - count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(r.Context(), confidential) + count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(ctx, confidential) if err != nil { return err } @@ -180,7 +186,7 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity // This means that we're able to remove a connection because it is the last configured credential. If it is // removed, the identity is no longer able to sign in. for _, l := range linked { - sr.UI.GetNodes().Append(NewUnlinkNode(l.Config().ID)) + sr.UI.GetNodes().Append(NewUnlinkNode(l.Config().ID, stringsx.Coalesce(l.Config().Label, l.Config().ID))) } } @@ -237,6 +243,11 @@ type updateSettingsFlowWithOidcMethod struct { // // required: false UpstreamParameters json.RawMessage `json:"upstream_parameters"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (p *updateSettingsFlowWithOidcMethod) GetFlowID() uuid.UUID { @@ -247,25 +258,29 @@ func (p *updateSettingsFlowWithOidcMethod) SetFlowID(rid uuid.UUID) { p.FlowID = rid.String() } -func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (*settings.UpdateContext, error) { +func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (_ *settings.UpdateContext, err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.oidc.strategy.Settings") + defer otelx.End(span, &err) + var p updateSettingsFlowWithOidcMethod if err := s.decoderSettings(&p, r); err != nil { return nil, err } + f.TransientPayload = p.TransientPayload ctxUpdate, err := settings.PrepareUpdate(s.d, w, r, f, ss, settings.ContinuityKey(s.SettingsStrategyID()), &p) if errors.Is(err, settings.ErrContinuePreviousAction) { - if !s.d.Config().SelfServiceStrategy(r.Context(), s.SettingsStrategyID()).Enabled { + if !s.d.Config().SelfServiceStrategy(ctx, s.SettingsStrategyID()).Enabled { return nil, errors.WithStack(herodot.ErrNotFound.WithReason(strategy.EndpointDisabledMessage)) } - if l := len(p.Link); l > 0 { + if len(p.Link) > 0 { if err := s.initLinkProvider(w, r, ctxUpdate, &p); err != nil { return nil, err } return ctxUpdate, nil - } else if u := len(p.Unlink); u > 0 { + } else if len(p.Unlink) > 0 { if err := s.unlinkProvider(w, r, ctxUpdate, &p); err != nil { return nil, err } @@ -278,49 +293,48 @@ func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings. return nil, s.handleSettingsError(w, r, ctxUpdate, &p, err) } - if len(p.Link+p.Unlink) == 0 { + if len(p.Link)+len(p.Unlink) == 0 { + span.SetAttributes(attribute.String("not_responsible_reason", "neither link nor unlink set")) return nil, errors.WithStack(flow.ErrStrategyNotResponsible) } - if !s.d.Config().SelfServiceStrategy(r.Context(), s.SettingsStrategyID()).Enabled { + if !s.d.Config().SelfServiceStrategy(ctx, s.SettingsStrategyID()).Enabled { return nil, errors.WithStack(herodot.ErrNotFound.WithReason(strategy.EndpointDisabledMessage)) } - if l, u := len(p.Link), len(p.Unlink); l > 0 && u > 0 { + switch l, u := len(p.Link), len(p.Unlink); { + case l > 0 && u > 0: return nil, s.handleSettingsError(w, r, ctxUpdate, &p, errors.WithStack(&jsonschema.ValidationError{ Message: "it is not possible to link and unlink providers in the same request", InstancePtr: "#/", })) - } else if l > 0 { + case l > 0: if err := s.initLinkProvider(w, r, ctxUpdate, &p); err != nil { return nil, err } return ctxUpdate, nil - } else if u > 0 { + case u > 0: if err := s.unlinkProvider(w, r, ctxUpdate, &p); err != nil { return nil, err } - return ctxUpdate, nil } - - return nil, s.handleSettingsError(w, r, ctxUpdate, &p, errors.WithStack(errors.WithStack(&jsonschema.ValidationError{ - Message: "missing properties: link, unlink", InstancePtr: "#/", - Context: &jsonschema.ValidationErrorContextRequired{Missing: []string{"link", "unlink"}}}))) + // this case should never be reached as we previously checked whether link and unlink are both empty + return nil, errors.WithStack(flow.ErrStrategyNotResponsible) } -func (s *Strategy) isLinkable(r *http.Request, ctxUpdate *settings.UpdateContext, toLink string) (*identity.Identity, error) { - providers, err := s.Config(r.Context()) +func (s *Strategy) isLinkable(ctx context.Context, ctxUpdate *settings.UpdateContext, toLink string) (*identity.Identity, error) { + providers, err := s.Config(ctx) if err != nil { return nil, err } - i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), ctxUpdate.Session.Identity.ID) + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ctxUpdate.Session.Identity.ID) if err != nil { return nil, err } - linkable, err := s.linkableProviders(r.Context(), r, providers, i) + linkable, err := s.linkableProviders(providers, i) if err != nil { return nil, err } @@ -340,31 +354,30 @@ func (s *Strategy) isLinkable(r *http.Request, ctxUpdate *settings.UpdateContext } func (s *Strategy) initLinkProvider(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithOidcMethod) error { - if _, err := s.isLinkable(r, ctxUpdate, p.Link); err != nil { + ctx := r.Context() + if _, err := s.isLinkable(ctx, ctxUpdate, p.Link); err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, err) } - if ctxUpdate.Session.AuthenticatedAt.Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(r.Context())).Before(time.Now()) { + if ctxUpdate.Session.AuthenticatedAt.Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(ctx)).Before(time.Now()) { return s.handleSettingsError(w, r, ctxUpdate, p, errors.WithStack(settings.NewFlowNeedsReAuth())) } - provider, err := s.provider(r.Context(), r, p.Link) + provider, err := s.provider(ctx, p.Link) if err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, err) } - c, err := provider.OAuth2(r.Context()) + req, err := s.validateFlow(ctx, r, ctxUpdate.Flow.ID) if err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, err) } - req, err := s.validateFlow(r.Context(), r, ctxUpdate.Flow.ID) + state, pkce, err := s.GenerateState(ctx, provider, ctxUpdate.Flow.ID) if err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, err) } - - state := generateState(ctxUpdate.Flow.ID.String()).String() - if err := s.d.ContinuityManager().Pause(r.Context(), w, r, sessionName, + if err := s.d.ContinuityManager().Pause(ctx, w, r, sessionName, continuity.WithPayload(&AuthCodeContainer{ State: state, FlowID: ctxUpdate.Flow.ID.String(), @@ -379,7 +392,11 @@ func (s *Strategy) initLinkProvider(w http.ResponseWriter, r *http.Request, ctxU return err } - codeURL := c.AuthCodeURL(state, append(UpstreamParameters(provider, up), provider.AuthCodeURLOptions(req)...)...) + codeURL, err := getAuthRedirectURL(ctx, provider, req, state, up, pkce) + if err != nil { + return s.handleSettingsError(w, r, ctxUpdate, p, err) + } + if x.IsJSONRequest(r) { s.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(codeURL)) } else { @@ -389,36 +406,21 @@ func (s *Strategy) initLinkProvider(w http.ResponseWriter, r *http.Request, ctxU return errors.WithStack(flow.ErrCompletedByStrategy) } -func (s *Strategy) linkProvider(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, token *oauth2.Token, claims *Claims, provider Provider) error { +func (s *Strategy) linkProvider(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, token *identity.CredentialsOIDCEncryptedTokens, claims *Claims, provider Provider) error { + ctx := r.Context() p := &updateSettingsFlowWithOidcMethod{ - Link: provider.Config().ID, FlowID: ctxUpdate.Flow.ID.String()} - if ctxUpdate.Session.AuthenticatedAt.Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(r.Context())).Before(time.Now()) { - return s.handleSettingsError(w, r, ctxUpdate, p, errors.WithStack(settings.NewFlowNeedsReAuth())) + Link: provider.Config().ID, FlowID: ctxUpdate.Flow.ID.String(), } - - i, err := s.isLinkable(r, ctxUpdate, p.Link) - if err != nil { - return s.handleSettingsError(w, r, ctxUpdate, p, err) - } - - var it string - if idToken, ok := token.Extra("id_token").(string); ok { - if it, err = s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(idToken)); err != nil { - return s.handleSettingsError(w, r, ctxUpdate, p, err) - } - } - - cat, err := s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(token.AccessToken)) - if err != nil { - return s.handleSettingsError(w, r, ctxUpdate, p, err) + if ctxUpdate.Session.AuthenticatedAt.Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(ctx)).Before(time.Now()) { + return s.handleSettingsError(w, r, ctxUpdate, p, errors.WithStack(settings.NewFlowNeedsReAuth())) } - crt, err := s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(token.RefreshToken)) + i, err := s.isLinkable(ctx, ctxUpdate, p.Link) if err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, err) } - if err := s.linkCredentials(r.Context(), i, it, cat, crt, provider.Config().ID, claims.Subject, provider.Config().OrganizationID); err != nil { + if err := s.linkCredentials(ctx, i, token, provider.Config().ID, claims.Subject, provider.Config().OrganizationID); err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, err) } @@ -432,21 +434,22 @@ func (s *Strategy) linkProvider(w http.ResponseWriter, r *http.Request, ctxUpdat } func (s *Strategy) unlinkProvider(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithOidcMethod) error { - if ctxUpdate.Session.AuthenticatedAt.Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(r.Context())).Before(time.Now()) { + ctx := r.Context() + if ctxUpdate.Session.AuthenticatedAt.Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(ctx)).Before(time.Now()) { return s.handleSettingsError(w, r, ctxUpdate, p, errors.WithStack(settings.NewFlowNeedsReAuth())) } - providers, err := s.Config(r.Context()) + providers, err := s.Config(ctx) if err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, err) } - i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), ctxUpdate.Session.Identity.ID) + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ctxUpdate.Session.Identity.ID) if err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, err) } - availableProviders, err := s.linkedProviders(r.Context(), r, providers, i) + availableProviders, err := s.linkedProviders(providers, i) if err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, err) } @@ -457,7 +460,7 @@ func (s *Strategy) unlinkProvider(w http.ResponseWriter, r *http.Request, ctxUpd return s.handleSettingsError(w, r, ctxUpdate, p, err) } - count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(r.Context(), i) + count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(ctx, i) if err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, err) } @@ -490,7 +493,6 @@ func (s *Strategy) unlinkProvider(w http.ResponseWriter, r *http.Request, ctxUpd creds.Config, err = json.Marshal(&identity.CredentialsOIDC{Providers: updatedProviders}) if err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, errors.WithStack(err)) - } i.Credentials[s.ID()] = *creds @@ -527,14 +529,13 @@ func (s *Strategy) Link(ctx context.Context, i *identity.Identity, credentialsCo if len(credentialsOIDCConfig.Providers) != 1 { return errors.New("No oidc provider was set") } - var credentialsOIDCProvider = credentialsOIDCConfig.Providers[0] + credentialsOIDCProvider := credentialsOIDCConfig.Providers[0] if err := s.linkCredentials( ctx, i, - credentialsOIDCProvider.InitialIDToken, - credentialsOIDCProvider.InitialAccessToken, - credentialsOIDCProvider.InitialRefreshToken, + // The tokens in this credential are coming from the existing identity. Hence, the values are already encrypted. + credentialsOIDCProvider.GetTokens(), credentialsOIDCProvider.Provider, credentialsOIDCProvider.Subject, credentialsOIDCProvider.Organization, diff --git a/selfservice/strategy/oidc/strategy_settings_test.go b/selfservice/strategy/oidc/strategy_settings_test.go index 753bae5321b8..af9d7cf8be75 100644 --- a/selfservice/strategy/oidc/strategy_settings_test.go +++ b/selfservice/strategy/oidc/strategy_settings_test.go @@ -7,6 +7,7 @@ import ( "context" _ "embed" "encoding/json" + "fmt" "net/http" "net/url" "strconv" @@ -15,6 +16,7 @@ import ( "github.com/ory/x/snapshotx" + "github.com/ory/kratos/driver" kratos "github.com/ory/kratos/internal/httpclient" "github.com/ory/kratos/ui/container" "github.com/ory/kratos/ui/node" @@ -28,7 +30,6 @@ import ( "github.com/ory/x/sqlxx" - "github.com/ory/kratos/driver" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" @@ -36,6 +37,7 @@ import ( "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/settings" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" "github.com/ory/kratos/selfservice/strategy/oidc" "github.com/ory/kratos/x" ) @@ -67,7 +69,9 @@ func TestSettingsStrategy(t *testing.T) { viperSetProviderConfig( t, conf, - newOIDCProvider(t, publicTS, remotePublic, remoteAdmin, "ory"), + newOIDCProvider(t, publicTS, remotePublic, remoteAdmin, "ory", func(c *oidc.Configuration) { + c.Label = "Ory" + }), newOIDCProvider(t, publicTS, remotePublic, remoteAdmin, "google"), newOIDCProvider(t, publicTS, remotePublic, remoteAdmin, "github"), orgSSO, @@ -183,7 +187,7 @@ func TestSettingsStrategy(t *testing.T) { t.Run("case=should not be able to fetch another user's data", func(t *testing.T) { req := newProfileFlow(t, agents["password"], "", time.Hour) - _, _, err := testhelpers.NewSDKCustomClient(publicTS, agents["oryer"]).FrontendApi.GetSettingsFlow(context.Background()).Id(req.ID.String()).Execute() + _, _, err := testhelpers.NewSDKCustomClient(publicTS, agents["oryer"]).FrontendAPI.GetSettingsFlow(context.Background()).Id(req.ID.String()).Execute() require.Error(t, err) assert.Contains(t, err.Error(), "403") }) @@ -191,7 +195,7 @@ func TestSettingsStrategy(t *testing.T) { t.Run("case=should fetch the settings request and expect data to be set appropriately", func(t *testing.T) { req := newProfileFlow(t, agents["password"], "", time.Hour) - rs, _, err := testhelpers.NewSDKCustomClient(publicTS, agents["password"]).FrontendApi.GetSettingsFlow(context.Background()).Id(req.ID.String()).Execute() + rs, _, err := testhelpers.NewSDKCustomClient(publicTS, agents["password"]).FrontendAPI.GetSettingsFlow(context.Background()).Id(req.ID.String()).Execute() require.NoError(t, err) // Check our sanity. Does the SDK relay the same info that we expect and got from the store? @@ -330,7 +334,7 @@ func TestSettingsStrategy(t *testing.T) { _, res, req := unlink(t, agent, provider) assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/login") - fa := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi + fa := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendAPI lf, _, err := fa.GetLoginFlow(context.Background()).Id(res.Request.URL.Query()["flow"][0]).Execute() require.NoError(t, err) @@ -456,7 +460,7 @@ func TestSettingsStrategy(t *testing.T) { updatedFlow, res, originalFlow := link(t, agent, provider) assert.Contains(t, res.Request.URL.String(), uiTS.URL) - updatedFlowSDK, _, err := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi.GetSettingsFlow(context.Background()).Id(originalFlow.Id).Execute() + updatedFlowSDK, _, err := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendAPI.GetSettingsFlow(context.Background()).Id(originalFlow.Id).Execute() require.NoError(t, err) require.EqualValues(t, flow.StateSuccess, updatedFlowSDK.State) @@ -483,7 +487,7 @@ func TestSettingsStrategy(t *testing.T) { _, res, req := link(t, agent, provider) assert.Contains(t, res.Request.URL.String(), uiTS.URL) - rs, _, err := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi.GetSettingsFlow(context.Background()).Id(req.Id).Execute() + rs, _, err := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendAPI.GetSettingsFlow(context.Background()).Id(req.Id).Execute() require.NoError(t, err) require.EqualValues(t, flow.StateSuccess, rs.State) @@ -567,7 +571,7 @@ func TestSettingsStrategy(t *testing.T) { _, res, req := link(t, agent, provider) assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/login") - fa := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendApi + fa := testhelpers.NewSDKCustomClient(publicTS, agents[agent]).FrontendAPI lf, _, err := fa.GetLoginFlow(context.Background()).Id(res.Request.URL.Query()["flow"][0]).Execute() require.NoError(t, err) @@ -609,21 +613,27 @@ func TestSettingsStrategy(t *testing.T) { } func TestPopulateSettingsMethod(t *testing.T) { - ctx := context.Background() - nreg := func(t *testing.T, conf *oidc.ConfigurationCollection) *driver.RegistryDefault { - c, reg := internal.NewFastRegistryWithMocks(t) - - testhelpers.SetDefaultIdentitySchema(c, "file://stub/registration.schema.json") - c.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") + t.Parallel() + nCtx := func(t *testing.T, conf *oidc.ConfigurationCollection) (*driver.RegistryDefault, context.Context) { + _, reg := internal.NewFastRegistryWithMocks(t) + ctx := context.Background() + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/registration.schema.json") + ctx = confighelpers.WithConfigValue(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/") + baseKey := fmt.Sprintf("%s.%s", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeOIDC) + + ctx = confighelpers.WithConfigValues(ctx, map[string]interface{}{ + baseKey + ".enabled": true, + baseKey + ".config": conf, + }) // Enabled per default: // conf.Set(ctx, configuration.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{"enabled": true}) - viperSetProviderConfig(t, c, conf.Providers...) - return reg + // viperSetProviderConfig(t, c, conf.Providers...) + return reg, ctx } - ns := func(t *testing.T, reg *driver.RegistryDefault) *oidc.Strategy { - ss, err := reg.SettingsStrategies(context.Background()).Strategy(identity.CredentialsTypeOIDC.String()) + ns := func(t *testing.T, reg *driver.RegistryDefault, ctx context.Context) *oidc.Strategy { + ss, err := reg.SettingsStrategies(ctx).Strategy(identity.CredentialsTypeOIDC.String()) require.NoError(t, err) return ss.(*oidc.Strategy) } @@ -632,13 +642,14 @@ func TestPopulateSettingsMethod(t *testing.T) { return &settings.Flow{Type: flow.TypeBrowser, ID: x.NewUUID(), UI: container.New("")} } - populate := func(t *testing.T, reg *driver.RegistryDefault, i *identity.Identity, req *settings.Flow) *container.Container { - require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) - require.NoError(t, ns(t, reg).PopulateSettingsMethod(new(http.Request), i, req)) - require.NotNil(t, req.UI) - require.NotNil(t, req.UI.Nodes) - assert.Equal(t, "POST", req.UI.Method) - return req.UI + populate := func(t *testing.T, reg *driver.RegistryDefault, ctx context.Context, i *identity.Identity, f *settings.Flow) *container.Container { + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i)) + req := new(http.Request) + require.NoError(t, ns(t, reg, ctx).PopulateSettingsMethod(req.WithContext(ctx), i, f)) + require.NotNil(t, f.UI) + require.NotNil(t, f.UI.Nodes) + assert.Equal(t, "POST", f.UI.Method) + return f.UI } defaultConfig := []oidc.Configuration{ @@ -648,12 +659,14 @@ func TestPopulateSettingsMethod(t *testing.T) { } t.Run("case=should not populate non-browser flow", func(t *testing.T) { - reg := nreg(t, &oidc.ConfigurationCollection{Providers: []oidc.Configuration{{Provider: "generic", ID: "github"}}}) + t.Parallel() + reg, ctx := nCtx(t, &oidc.ConfigurationCollection{Providers: []oidc.Configuration{{Provider: "generic", ID: "github"}}}) i := &identity.Identity{Traits: []byte(`{"subject":"foo@bar.com"}`)} - require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) - req := &settings.Flow{Type: flow.TypeAPI, ID: x.NewUUID(), UI: container.New("")} - require.NoError(t, ns(t, reg).PopulateSettingsMethod(new(http.Request), i, req)) - require.Empty(t, req.UI.Nodes) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i)) + f := &settings.Flow{Type: flow.TypeAPI, ID: x.NewUUID(), UI: container.New("")} + req := new(http.Request) + require.NoError(t, ns(t, reg, ctx).PopulateSettingsMethod(req.WithContext(ctx), i, f)) + require.Empty(t, f.UI.Nodes) }) for k, tc := range []struct { @@ -674,25 +687,25 @@ func TestPopulateSettingsMethod(t *testing.T) { }, e: node.Nodes{ node.NewCSRFNode(x.FakeCSRFToken), - oidc.NewLinkNode("github"), + oidc.NewLinkNode("github", "github"), }, }, { c: defaultConfig, e: node.Nodes{ node.NewCSRFNode(x.FakeCSRFToken), - oidc.NewLinkNode("facebook"), - oidc.NewLinkNode("google"), - oidc.NewLinkNode("github"), + oidc.NewLinkNode("facebook", "facebook"), + oidc.NewLinkNode("google", "google"), + oidc.NewLinkNode("github", "github"), }, }, { c: defaultConfig, e: node.Nodes{ node.NewCSRFNode(x.FakeCSRFToken), - oidc.NewLinkNode("facebook"), - oidc.NewLinkNode("google"), - oidc.NewLinkNode("github"), + oidc.NewLinkNode("facebook", "facebook"), + oidc.NewLinkNode("google", "google"), + oidc.NewLinkNode("github", "github"), }, i: &identity.Credentials{Type: identity.CredentialsTypeOIDC, Identifiers: []string{}, Config: []byte(`{}`)}, }, @@ -700,8 +713,8 @@ func TestPopulateSettingsMethod(t *testing.T) { c: defaultConfig, e: node.Nodes{ node.NewCSRFNode(x.FakeCSRFToken), - oidc.NewLinkNode("facebook"), - oidc.NewLinkNode("github"), + oidc.NewLinkNode("facebook", "facebook"), + oidc.NewLinkNode("github", "github"), }, i: &identity.Credentials{Type: identity.CredentialsTypeOIDC, Identifiers: []string{ "google:1234", @@ -711,9 +724,9 @@ func TestPopulateSettingsMethod(t *testing.T) { c: defaultConfig, e: node.Nodes{ node.NewCSRFNode(x.FakeCSRFToken), - oidc.NewLinkNode("facebook"), - oidc.NewLinkNode("github"), - oidc.NewUnlinkNode("google"), + oidc.NewLinkNode("facebook", "facebook"), + oidc.NewLinkNode("github", "github"), + oidc.NewUnlinkNode("google", "google"), }, withpw: true, i: &identity.Credentials{ @@ -727,9 +740,9 @@ func TestPopulateSettingsMethod(t *testing.T) { c: defaultConfig, e: node.Nodes{ node.NewCSRFNode(x.FakeCSRFToken), - oidc.NewLinkNode("github"), - oidc.NewUnlinkNode("google"), - oidc.NewUnlinkNode("facebook"), + oidc.NewLinkNode("github", "github"), + oidc.NewUnlinkNode("google", "google"), + oidc.NewUnlinkNode("facebook", "facebook"), }, i: &identity.Credentials{ Type: identity.CredentialsTypeOIDC, Identifiers: []string{ @@ -739,9 +752,37 @@ func TestPopulateSettingsMethod(t *testing.T) { Config: []byte(`{"providers":[{"provider":"google","subject":"1234"},{"provider":"facebook","subject":"1234"}]}`), }, }, + { + c: []oidc.Configuration{ + {Provider: "generic", ID: "labeled", Label: "Labeled"}, + }, + e: node.Nodes{ + node.NewCSRFNode(x.FakeCSRFToken), + oidc.NewLinkNode("labeled", "Labeled"), + }, + }, + { + c: []oidc.Configuration{ + {Provider: "generic", ID: "labeled", Label: "Labeled"}, + {Provider: "generic", ID: "facebook"}, + }, + e: node.Nodes{ + node.NewCSRFNode(x.FakeCSRFToken), + oidc.NewUnlinkNode("labeled", "Labeled"), + oidc.NewUnlinkNode("facebook", "facebook"), + }, + i: &identity.Credentials{ + Type: identity.CredentialsTypeOIDC, Identifiers: []string{ + "labeled:1234", + "facebook:1234", + }, + Config: []byte(`{"providers":[{"provider":"labeled","subject":"1234"},{"provider":"facebook","subject":"1234"}]}`), + }, + }, } { t.Run("iteration="+strconv.Itoa(k), func(t *testing.T) { - reg := nreg(t, &oidc.ConfigurationCollection{Providers: tc.c}) + t.Parallel() + reg, ctx := nCtx(t, &oidc.ConfigurationCollection{Providers: tc.c}) i := &identity.Identity{ Traits: []byte(`{"subject":"foo@bar.com"}`), Credentials: make(map[identity.CredentialsType]identity.Credentials, 2), @@ -756,7 +797,7 @@ func TestPopulateSettingsMethod(t *testing.T) { Config: []byte(`{"hashed_password":"$argon2id$..."}`), } } - actual := populate(t, reg, i, nr()) + actual := populate(t, reg, ctx, i, nr()) assert.EqualValues(t, tc.e, actual.Nodes) }) } diff --git a/selfservice/strategy/oidc/strategy_state_test.go b/selfservice/strategy/oidc/strategy_state_test.go deleted file mode 100644 index 28302400861d..000000000000 --- a/selfservice/strategy/oidc/strategy_state_test.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package oidc - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/ory/kratos/x" -) - -func TestGenerateState(t *testing.T) { - flowID := x.NewUUID().String() - state := generateState(flowID).String() - assert.NotEmpty(t, state) - - s, err := parseState(state) - require.NoError(t, err) - assert.Equal(t, flowID, s.FlowID) - assert.NotEmpty(t, s.Data) -} diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index 951f061995e3..25faf0e91a55 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -13,11 +13,18 @@ import ( "net/http/cookiejar" "net/http/httptest" "net/url" + "slices" "strconv" "strings" "testing" "time" + "github.com/davecgh/go-spew/spew" + "github.com/pkg/errors" + "github.com/samber/lo" + "golang.org/x/oauth2" + + "github.com/ory/kratos/selfservice/hook/hooktest" "github.com/ory/x/sqlxx" "github.com/ory/kratos/hydra" @@ -78,10 +85,20 @@ func TestStrategy(t *testing.T) { t, conf, newOIDCProvider(t, ts, remotePublic, remoteAdmin, "valid"), + newOIDCProvider(t, ts, remotePublic, remoteAdmin, "valid2"), newOIDCProvider(t, ts, remotePublic, remoteAdmin, "secondProvider"), newOIDCProvider(t, ts, remotePublic, remoteAdmin, "claimsViaUserInfo", func(c *oidc.Configuration) { c.ClaimsSource = oidc.ClaimsSourceUserInfo }), + newOIDCProvider(t, ts, remotePublic, remoteAdmin, "neverPKCE", func(c *oidc.Configuration) { + c.PKCE = "never" + }), + newOIDCProvider(t, ts, remotePublic, remoteAdmin, "autoPKCE", func(c *oidc.Configuration) { + c.PKCE = "auto" + }), + newOIDCProvider(t, ts, remotePublic, remoteAdmin, "forcePKCE", func(c *oidc.Configuration) { + c.PKCE = "force" + }), oidc.Configuration{ Provider: "generic", ID: "invalid-issuer", @@ -126,14 +143,13 @@ func TestStrategy(t *testing.T) { assert.Equal(t, "POST", config.Method) - var found bool - for _, field := range config.Nodes { - if strings.Contains(field.ID(), "provider") && field.GetValue() == provider { - found = true - break + var providers []interface{} + for _, nodes := range config.Nodes { + if strings.Contains(nodes.ID(), "provider") { + providers = append(providers, nodes.GetValue()) } } - require.True(t, found, "%+v", assertx.PrettifyJSONPayload(t, config)) + require.Contains(t, providers, provider, "%+v", assertx.PrettifyJSONPayload(t, config)) return config.Action } @@ -146,9 +162,9 @@ func TestStrategy(t *testing.T) { return ts.URL + login.RouteSubmitFlow + "?flow=" + flowID.String() } - makeRequestWithCookieJar := func(t *testing.T, provider string, action string, fv url.Values, jar *cookiejar.Jar) (*http.Response, []byte) { + makeRequestWithCookieJar := func(t *testing.T, provider string, action string, fv url.Values, jar *cookiejar.Jar, checkRedirect testhelpers.CheckRedirectFunc) (*http.Response, []byte) { fv.Set("provider", provider) - res, err := testhelpers.NewClientWithCookieJar(t, jar, false).PostForm(action, fv) + res, err := testhelpers.NewClientWithCookieJar(t, jar, checkRedirect).PostForm(action, fv) require.NoError(t, err, action) body, err := io.ReadAll(res.Body) @@ -161,12 +177,12 @@ func TestStrategy(t *testing.T) { } makeRequest := func(t *testing.T, provider string, action string, fv url.Values) (*http.Response, []byte) { - return makeRequestWithCookieJar(t, provider, action, fv, nil) + return makeRequestWithCookieJar(t, provider, action, fv, nil, nil) } makeJSONRequest := func(t *testing.T, provider string, action string, fv url.Values) (*http.Response, []byte) { fv.Set("provider", provider) - client := testhelpers.NewClientWithCookieJar(t, nil, false) + client := testhelpers.NewClientWithCookieJar(t, nil, nil) req, err := http.NewRequest("POST", action, strings.NewReader(fv.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") @@ -183,7 +199,7 @@ func TestStrategy(t *testing.T) { return res, body } - makeAPICodeFlowRequest := func(t *testing.T, provider, action string) (returnToCode string) { + makeAPICodeFlowRequest := func(t *testing.T, provider, action string) (returnToURL *url.URL) { res, err := testhelpers.NewDebugClient(t).Post(action, "application/json", strings.NewReader(fmt.Sprintf(`{ "method": "oidc", "provider": %q @@ -193,16 +209,13 @@ func TestStrategy(t *testing.T) { var changeLocation flow.BrowserLocationChangeRequiredError require.NoError(t, json.NewDecoder(res.Body).Decode(&changeLocation)) - res, err = testhelpers.NewClientWithCookieJar(t, nil, true).Get(changeLocation.RedirectBrowserTo) + res, err = testhelpers.NewClientWithCookieJar(t, nil, nil).Get(changeLocation.RedirectBrowserTo) require.NoError(t, err) - returnToURL := res.Request.URL + returnToURL = res.Request.URL assert.True(t, strings.HasPrefix(returnToURL.String(), returnTS.URL+"/app_code")) - code := returnToURL.Query().Get("code") - assert.NotEmpty(t, code, "code query param was empty in the return_to URL") - - return code + return returnToURL } exchangeCodeForToken := func(t *testing.T, codes sessiontokenexchange.Codes) (codeResponse session.CodeExchangeResponse, err error) { @@ -238,7 +251,7 @@ func TestStrategy(t *testing.T) { // assert ui error (redirect to login/registration ui endpoint) assertUIError := func(t *testing.T, res *http.Response, body []byte, reason string) { - require.Contains(t, res.Request.URL.String(), uiTS.URL, "status: %d, body: %s", res.StatusCode, body) + require.Contains(t, res.Request.URL.String(), uiTS.URL, "Redirect does not point to UI server. Status: %d, body: %s", res.StatusCode, body) assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), reason, "%s", prettyJSON(t, body)) } @@ -423,21 +436,34 @@ func TestStrategy(t *testing.T) { } t.Run("case=should pass registration", func(t *testing.T) { + postRegistrationWebhook := hooktest.NewServer() + t.Cleanup(postRegistrationWebhook.Close) + postRegistrationWebhook.SetConfig(t, conf.GetProvider(ctx), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeOIDC.String())) + transientPayload := `{"data": "registration-one"}` + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) action := assertFormValues(t, r.ID, "valid") - res, body := makeRequest(t, "valid", action, url.Values{}) + res, body := makeRequest(t, "valid", action, url.Values{"transient_payload": {transientPayload}}) assertIdentity(t, res, body) expectTokens(t, "valid", body) + postRegistrationWebhook.AssertTransientPayload(t, transientPayload) }) t.Run("case=try another registration", func(t *testing.T) { + transientPayload := `{"data": "registration-two"}` + postLoginWebhook := hooktest.NewServer() + t.Cleanup(postLoginWebhook.Close) + postLoginWebhook.SetConfig(t, conf.GetProvider(ctx), config.HookStrategyKey(config.ViperKeySelfServiceLoginAfter, identity.CredentialsTypeOIDC.String())) + returnTo := fmt.Sprintf("%s/home?query=true", returnTS.URL) r := newBrowserRegistrationFlow(t, fmt.Sprintf("%s?return_to=%s", returnTS.URL, url.QueryEscape(returnTo)), time.Minute) action := assertFormValues(t, r.ID, "valid") - res, body := makeRequest(t, "valid", action, url.Values{}) + res, body := makeRequest(t, "valid", action, url.Values{"transient_payload": {transientPayload}}) assert.Equal(t, returnTo, res.Request.URL.String()) assertIdentity(t, res, body) expectTokens(t, "valid", body) + + postLoginWebhook.AssertTransientPayload(t, transientPayload) }) }) @@ -456,39 +482,335 @@ func TestStrategy(t *testing.T) { return id } + t.Run("case=force PKCE", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "forcePKCE") + subject = "force-pkce@ory.sh" + scope = []string{"openid", "offline"} + var redirects []*http.Request + res, body := makeRequestWithCookieJar(t, "forcePKCE", action, url.Values{}, nil, func(_ *http.Request, via []*http.Request) error { + redirects = via + return nil + }) + require.GreaterOrEqual(t, len(redirects), 3) + assert.Contains(t, redirects[1].URL.String(), "/oauth2/auth") + assert.Contains(t, redirects[1].URL.String(), "code_challenge_method=S256") + assert.Contains(t, redirects[1].URL.String(), "code_challenge=") + assert.Equal(t, redirects[len(redirects)-1].URL.Path, "/self-service/methods/oidc/callback") + + assertIdentity(t, res, body) + expectTokens(t, "forcePKCE", body) + assert.Equal(t, "forcePKCE", gjson.GetBytes(body, "authentication_methods.0.provider").String(), "%s", body) + }) + t.Run("case=force PKCE, invalid verifier", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "forcePKCE") + subject = "force-pkce@ory.sh" + scope = []string{"openid", "offline"} + verifierFalsified := false + res, body := makeRequestWithCookieJar(t, "forcePKCE", action, url.Values{}, nil, func(req *http.Request, via []*http.Request) error { + if req.URL.Path == "/oauth2/auth" && !verifierFalsified { + q := req.URL.Query() + require.NotEmpty(t, q.Get("code_challenge")) + require.Equal(t, "S256", q.Get("code_challenge_method")) + q.Set("code_challenge", oauth2.S256ChallengeFromVerifier(oauth2.GenerateVerifier())) + req.URL.RawQuery = q.Encode() + verifierFalsified = true + } + return nil + }) + require.True(t, verifierFalsified) + assertSystemErrorWithMessage(t, res, body, http.StatusInternalServerError, "The PKCE code challenge did not match the code verifier.") + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()) + }) + t.Run("case=force PKCE, code challenge params removed from initial redirect", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "forcePKCE") + subject = "force-pkce@ory.sh" + scope = []string{"openid", "offline"} + challengeParamsRemoved := false + res, body := makeRequestWithCookieJar(t, "forcePKCE", action, url.Values{}, nil, func(req *http.Request, via []*http.Request) error { + if req.URL.Path == "/oauth2/auth" && !challengeParamsRemoved { + q := req.URL.Query() + require.NotEmpty(t, q.Get("code_challenge")) + require.Equal(t, "S256", q.Get("code_challenge_method")) + q.Del("code_challenge") + q.Del("code_challenge_method") + req.URL.RawQuery = q.Encode() + challengeParamsRemoved = true + } + return nil + }) + require.True(t, challengeParamsRemoved) + assertSystemErrorWithMessage(t, res, body, http.StatusInternalServerError, "The PKCE code challenge did not match the code verifier.") + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()) + }) + t.Run("case=PKCE prevents authorization code injection attacks", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "forcePKCE") + subject = "attacker@ory.sh" + scope = []string{"openid", "offline"} + var code string + _, err := testhelpers.NewClientWithCookieJar(t, nil, func(req *http.Request, via []*http.Request) error { + if req.URL.Query().Has("code") { + code = req.URL.Query().Get("code") + return errors.New("code intercepted") + } + return nil + }).PostForm(action, url.Values{"provider": {"forcePKCE"}}) + require.ErrorContains(t, err, "code intercepted") + require.NotEmpty(t, code) // code now contains a valid authorization code + + r2 := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + action = assertFormValues(t, r2.ID, "forcePKCE") + jar, err := cookiejar.New(nil) // must capture the continuity cookie + require.NoError(t, err) + var redirectURI, state string + _, err = testhelpers.NewClientWithCookieJar(t, jar, func(req *http.Request, via []*http.Request) error { + if req.URL.Path == "/oauth2/auth" { + redirectURI = req.URL.Query().Get("redirect_uri") + state = req.URL.Query().Get("state") + return errors.New("stop before redirect to Authorization URL") + } + return nil + }).PostForm(action, url.Values{"provider": {"forcePKCE"}}) + require.ErrorContains(t, err, "stop") + require.NotEmpty(t, redirectURI) + require.NotEmpty(t, state) + res, err := testhelpers.NewClientWithCookieJar(t, jar, nil).Get(redirectURI + "?code=" + code + "&state=" + state) + require.NoError(t, err) + body := x.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + assertSystemErrorWithMessage(t, res, body, http.StatusInternalServerError, "The PKCE code challenge did not match the code verifier.") + }) + t.Run("case=confused providers are detected", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + subject = "attacker@ory.sh" + scope = []string{"openid", "offline"} + redirectConfused := false + res, err := testhelpers.NewClientWithCookieJar(t, nil, func(req *http.Request, via []*http.Request) error { + if req.URL.Query().Has("code") { + req.URL.Path = strings.Replace(req.URL.Path, "valid", "valid2", 1) + redirectConfused = true + } + return nil + }).PostForm(action, url.Values{"provider": {"valid"}}) + require.True(t, redirectConfused) + require.NoError(t, err) + body := x.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + + assertSystemErrorWithReason(t, res, body, http.StatusBadRequest, "provider mismatch between internal state and URL") + }) + t.Run("case=automatic PKCE", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "autoPKCE") + subject = "auto-pkce@ory.sh" + scope = []string{"openid", "offline"} + var redirects []*http.Request + res, body := makeRequestWithCookieJar(t, "autoPKCE", action, url.Values{}, nil, func(_ *http.Request, via []*http.Request) error { + redirects = via + return nil + }) + require.GreaterOrEqual(t, len(redirects), 3) + assert.Contains(t, redirects[1].URL.String(), "/oauth2/auth") + assert.Contains(t, redirects[1].URL.String(), "code_challenge_method=S256") + assert.Contains(t, redirects[1].URL.String(), "code_challenge=") + assert.Equal(t, redirects[len(redirects)-1].URL.Path, "/self-service/methods/oidc/callback/autoPKCE") + + assertIdentity(t, res, body) + expectTokens(t, "autoPKCE", body) + assert.Equal(t, "autoPKCE", gjson.GetBytes(body, "authentication_methods.0.provider").String(), "%s", body) + }) + t.Run("case=disabled PKCE", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "neverPKCE") + subject = "never-pkce@ory.sh" + scope = []string{"openid", "offline"} + var redirects []*http.Request + res, body := makeRequestWithCookieJar(t, "neverPKCE", action, url.Values{}, nil, func(_ *http.Request, via []*http.Request) error { + redirects = via + return nil + }) + require.GreaterOrEqual(t, len(redirects), 3) + assert.Contains(t, redirects[1].URL.String(), "/oauth2/auth") + assert.NotContains(t, redirects[1].URL.String(), "code_challenge_method=") + assert.NotContains(t, redirects[1].URL.String(), "code_challenge=") + assert.Equal(t, redirects[len(redirects)-1].URL.Path, "/self-service/methods/oidc/callback/neverPKCE") + + assertIdentity(t, res, body) + expectTokens(t, "neverPKCE", body) + assert.Equal(t, "neverPKCE", gjson.GetBytes(body, "authentication_methods.0.provider").String(), "%s", body) + }) + t.Run("case=register and then login", func(t *testing.T) { + postRegistrationWebhook := hooktest.NewServer() + t.Cleanup(postRegistrationWebhook.Close) + postLoginWebhook := hooktest.NewServer() + t.Cleanup(postLoginWebhook.Close) + + postRegistrationWebhook.SetConfig(t, conf.GetProvider(ctx), + config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeOIDC.String())) + postLoginWebhook.SetConfig(t, conf.GetProvider(ctx), + config.HookStrategyKey(config.ViperKeySelfServiceLoginAfter, config.HookGlobal)) + subject = "register-then-login@ory.sh" scope = []string{"openid", "offline"} t.Run("case=should pass registration", func(t *testing.T) { + transientPayload := `{"data": "registration"}` r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) action := assertFormValues(t, r.ID, "valid") - res, body := makeRequest(t, "valid", action, url.Values{}) + res, body := makeRequest(t, "valid", action, url.Values{ + "transient_payload": {transientPayload}, + }) assertIdentity(t, res, body) expectTokens(t, "valid", body) assert.Equal(t, "valid", gjson.GetBytes(body, "authentication_methods.0.provider").String(), "%s", body) + + postRegistrationWebhook.AssertTransientPayload(t, transientPayload) }) t.Run("case=should pass login", func(t *testing.T) { + transientPayload := `{"data": "login"}` r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) action := assertFormValues(t, r.ID, "valid") - res, body := makeRequest(t, "valid", action, url.Values{}) + res, body := makeRequest(t, "valid", action, url.Values{ + "transient_payload": {transientPayload}, + }) assertIdentity(t, res, body) expectTokens(t, "valid", body) assert.Equal(t, "valid", gjson.GetBytes(body, "authentication_methods.0.provider").String(), "%s", body) + + postLoginWebhook.AssertTransientPayload(t, transientPayload) + }) + + t.Run("case=should pass double submit", func(t *testing.T) { + // This test checks that the continuity manager uses a grace period to handle potential double-submit issues. + // + // It addresses issues where Facebook and Apple consent screens on mobile behave in a way that makes it + // easy for users to experience double-submit issues. + j, err := cookiejar.New(nil) + require.NoError(t, err) + + makeInitialRequest := func(t *testing.T, provider, action string, fv url.Values) (*http.Response, []byte, []string) { + fv.Set("provider", provider) + + var lastVia []*http.Request + hc := &http.Client{ + Jar: j, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + lastVia = via + return nil + }, + } + res, err := hc.PostForm(action, fv) + require.NoError(t, err, action) + + body, err := io.ReadAll(res.Body) + require.NoError(t, res.Body.Close()) + require.NoError(t, err) + require.NotEmpty(t, lastVia) + + vias := make([]string, len(lastVia)) + for k, v := range lastVia { + vias[k] = v.URL.String() + } + + return res, body, vias + } + + r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + + // First login + res, body, via := makeInitialRequest(t, "valid", action, url.Values{}) + assertIdentity(t, res, body) + expectTokens(t, "valid", body) + assert.Equal(t, "valid", gjson.GetBytes(body, "authentication_methods.0.provider").String(), "%s", body) + + // We fetch the URL which includes the `?code` query parameter. + result := lo.Filter(via, func(s string, _ int) bool { + return strings.Contains(s, "code=") + }) + require.Len(t, result, 1) + + // And call that URL again. What's interesting here is that the whole requets passes because we are already authenticated. + // + // In this scenario, Ory Kratos correctly forwards the user to the return URL, which in our case returns the identity. + // + // We essentially run into this bit: + // + // if authenticated, err := s.alreadyAuthenticated(w, r, req); err != nil { + // s.forwardError(w, r, req, s.handleError(w, , r, req, pid, nil, err)) + // } else if authenticated { + // return <-- we end up here on the second call + // } + res, err = (&http.Client{Jar: j}).Get(result[0]) + require.NoError(t, err) + body, err = io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + + assertIdentity(t, res, body) + expectTokens(t, "valid", body) + assert.Equal(t, "valid", gjson.GetBytes(body, "authentication_methods.0.provider").String(), "%s", body) + + // Trying this flow again without the Ory Session cookie will fail as we run into code reuse: + cookies := j.Cookies(urlx.ParseOrPanic(ts.URL)) + t.Logf("Cookies: %s", spew.Sdump(cookies)) + + secondJar, err := cookiejar.New(nil) + require.NoError(t, err) + + secondJar.SetCookies(urlx.ParseOrPanic(ts.URL), lo.Filter(cookies, func(item *http.Cookie, index int) bool { + return item.Name != "ory_kratos_session" + })) + + cookies = secondJar.Cookies(urlx.ParseOrPanic(ts.URL)) + t.Logf("Cookies after: %s", spew.Sdump(cookies)) + + // Doing the request but this time without the Ory Session Cookie. This may be the case in scenarios where we run into race conditions + // where the server sent a response but the client did not process it. + res, err = (&http.Client{Jar: secondJar}).Get(result[0]) + require.NoError(t, err) + body, err = io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + + assert.Contains(t, string(body), "The authorization code has already been used", "%s", body) }) }) t.Run("case=login without registered account", func(t *testing.T) { + postRegistrationWebhook := hooktest.NewServer() + t.Cleanup(postRegistrationWebhook.Close) + postLoginWebhook := hooktest.NewServer() + t.Cleanup(postLoginWebhook.Close) + + postRegistrationWebhook.SetConfig(t, conf.GetProvider(ctx), + config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeOIDC.String())) + postLoginWebhook.SetConfig(t, conf.GetProvider(ctx), + config.HookStrategyKey(config.ViperKeySelfServiceLoginAfter, config.HookGlobal)) + subject = "login-without-register@ory.sh" scope = []string{"openid"} t.Run("case=should pass login", func(t *testing.T) { + transientPayload := `{"data": "login to registration"}` + r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) action := assertFormValues(t, r.ID, "valid") - res, body := makeRequest(t, "valid", action, url.Values{}) + res, body := makeRequest(t, "valid", action, url.Values{ + "transient_payload": {transientPayload}, + }) assertIdentity(t, res, body) assert.Equal(t, "valid", gjson.GetBytes(body, "authentication_methods.0.provider").String(), "%s", body) + + assert.Empty(t, postLoginWebhook.LastBody, + "post login hook should not have been called, because this was a registration flow") + postRegistrationWebhook.AssertTransientPayload(t, transientPayload) }) }) @@ -514,12 +836,15 @@ func TestStrategy(t *testing.T) { t.Run("suite=API with session token exchange code", func(t *testing.T) { scope = []string{"openid"} - loginOrRegister := func(t *testing.T, id uuid.UUID, code string) { + loginOrRegister := func(t *testing.T, flowID uuid.UUID, code string) { _, err := exchangeCodeForToken(t, sessiontokenexchange.Codes{InitCode: code}) require.Error(t, err) - action := assertFormValues(t, id, "valid") - returnToCode := makeAPICodeFlowRequest(t, "valid", action) + action := assertFormValues(t, flowID, "valid") + returnToURL := makeAPICodeFlowRequest(t, "valid", action) + returnToCode := returnToURL.Query().Get("code") + assert.NotEmpty(t, code, "code query param was empty in the return_to URL") + codeResponse, err := exchangeCodeForToken(t, sessiontokenexchange.Codes{ InitCode: code, ReturnToCode: returnToCode, @@ -529,11 +854,11 @@ func TestStrategy(t *testing.T) { assert.NotEmpty(t, codeResponse.Token) assert.Equal(t, subject, gjson.GetBytes(codeResponse.Session.Identity.Traits, "subject").String()) } - register := func(t *testing.T) { + performRegistration := func(t *testing.T) { f := newAPIRegistrationFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", 1*time.Minute) loginOrRegister(t, f.ID, f.SessionTokenExchangeCode) } - login := func(t *testing.T) { + performLogin := func(t *testing.T) { f := newAPILoginFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", 1*time.Minute) loginOrRegister(t, f.ID, f.SessionTokenExchangeCode) } @@ -543,16 +868,16 @@ func TestStrategy(t *testing.T) { first, then func(*testing.T) }{{ name: "login-twice", - first: login, then: login, + first: performLogin, then: performLogin, }, { name: "login-then-register", - first: login, then: register, + first: performLogin, then: performRegistration, }, { name: "register-then-login", - first: register, then: login, + first: performRegistration, then: performLogin, }, { name: "register-twice", - first: register, then: register, + first: performRegistration, then: performRegistration, }} { t.Run("case="+tc.name, func(t *testing.T) { subject = tc.name + "-api-code-testing@ory.sh" @@ -560,6 +885,31 @@ func TestStrategy(t *testing.T) { tc.then(t) }) } + t.Run("case=should use redirect_to URL on failure", func(t *testing.T) { + ctx := context.Background() + subject = "existing-subject-api-code-testing@ory.sh" + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ + Identifiers: []string{subject}, + }) + i.Traits = identity.Traits(`{"subject":"` + subject + `"}`) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i)) + + f := newAPILoginFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", 1*time.Minute) + + _, err := exchangeCodeForToken(t, sessiontokenexchange.Codes{InitCode: f.SessionTokenExchangeCode}) + require.Error(t, err) + + action := assertFormValues(t, f.ID, "valid") + returnToURL := makeAPICodeFlowRequest(t, "valid", action) + returnedFlow := returnToURL.Query().Get("flow") + + require.NotEmpty(t, returnedFlow, "flow query param was empty in the return_to URL") + loginFlow, err := reg.LoginFlowPersister().GetLoginFlow(ctx, uuid.FromStringOrNil(returnedFlow)) + require.NoError(t, err) + assert.Equal(t, text.InfoSelfServiceLoginLink, loginFlow.UI.Messages[0].ID) + }) }) t.Run("case=submit id_token during registration or login", func(t *testing.T) { @@ -576,7 +926,7 @@ func TestStrategy(t *testing.T) { Mapper: "file://./stub/oidc.facebook.jsonnet", }, ) - t.Cleanup(oidc.RegisterTestProvider("test-provider")) + oidc.RegisterTestProvider(t, "test-provider") cl := http.Client{} @@ -803,7 +1153,7 @@ func TestStrategy(t *testing.T) { action := assertFormValues(t, r.ID, "valid") fv := url.Values{} fv.Set("provider", "valid") - res, err := testhelpers.NewClientWithCookieJar(t, nil, false).PostForm(action, fv) + res, err := testhelpers.NewClientWithCookieJar(t, nil, nil).PostForm(action, fv) require.NoError(t, err) // Expect to be returned to the hydra instance, that instantiated the request assert.Equal(t, hydra.FakePostLoginURL, res.Request.URL.String()) @@ -815,34 +1165,54 @@ func TestStrategy(t *testing.T) { scope = []string{"openid"} t.Run("case=should pass registration", func(t *testing.T) { + postRegistrationWebhook := hooktest.NewServer() + t.Cleanup(postRegistrationWebhook.Close) + postRegistrationWebhook.SetConfig(t, conf.GetProvider(ctx), config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypeOIDC.String())) + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) action := assertFormValues(t, r.ID, "valid") - res, body := makeRequest(t, "valid", action, url.Values{}) + transientPayload := `{"data": "registration-one"}` + res, body := makeRequest(t, "valid", action, url.Values{"transient_payload": {transientPayload}}) assertIdentity(t, res, body) + postRegistrationWebhook.AssertTransientPayload(t, transientPayload) }) t.Run("case=should pass second time registration", func(t *testing.T) { + postLoginWebhook := hooktest.NewServer() + t.Cleanup(postLoginWebhook.Close) + postLoginWebhook.SetConfig(t, conf.GetProvider(ctx), config.HookStrategyKey(config.ViperKeySelfServiceLoginAfter, identity.CredentialsTypeOIDC.String())) + r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) action := assertFormValues(t, r.ID, "valid") - res, body := makeRequest(t, "valid", action, url.Values{}) + transientPayload := `{"data": "registration-two"}` + res, body := makeRequest(t, "valid", action, url.Values{"transient_payload": {transientPayload}}) assertIdentity(t, res, body) + postLoginWebhook.AssertTransientPayload(t, transientPayload) }) t.Run("case=should pass third time registration with return to", func(t *testing.T) { + postLoginWebhook := hooktest.NewServer() + t.Cleanup(postLoginWebhook.Close) + postLoginWebhook.SetConfig(t, conf.GetProvider(ctx), config.HookStrategyKey(config.ViperKeySelfServiceLoginAfter, identity.CredentialsTypeOIDC.String())) + returnTo := "/foo" r := newBrowserLoginFlow(t, fmt.Sprintf("%s?return_to=%s", returnTS.URL, returnTo), time.Minute) action := assertFormValues(t, r.ID, "valid") - res, body := makeRequest(t, "valid", action, url.Values{}) + transientPayload := `{"data": "registration-three"}` + res, body := makeRequest(t, "valid", action, url.Values{"transient_payload": {transientPayload}}) assert.True(t, strings.HasSuffix(res.Request.URL.String(), returnTo)) assertIdentity(t, res, body) + postLoginWebhook.AssertTransientPayload(t, transientPayload) }) }) t.Run("case=register, merge, and complete data", func(t *testing.T) { - for _, tc := range []struct{ name, provider string }{ {name: "idtoken", provider: "valid"}, {name: "userinfo", provider: "claimsViaUserInfo"}, + {name: "disable-pkce", provider: "neverPKCE"}, + {name: "auto-pkce", provider: "autoPKCE"}, + {name: "force-pkce", provider: "forcePKCE"}, } { subject = fmt.Sprintf("incomplete-data@%s.ory.sh", tc.name) scope = []string{"openid"} @@ -876,38 +1246,85 @@ func TestStrategy(t *testing.T) { }) }) } - }) - t.Run("case=should fail to register and return fresh login flow if email is already being used by password credentials", func(t *testing.T) { - subject = "email-exist-with-password-strategy@ory.sh" - scope = []string{"openid"} + for _, loginHintsEnabled := range []bool{true, false} { + t.Run("login-hints-enabled="+strconv.FormatBool(loginHintsEnabled), func(t *testing.T) { + t.Run("case=should fail to register and return fresh login flow if email is already being used by password credentials", func(t *testing.T) { + subject = "email-exist-with-password-strategy-lh-" + strconv.FormatBool(loginHintsEnabled) + "@ory.sh" + scope = []string{"openid"} + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationLoginHints, strconv.FormatBool(loginHintsEnabled)) + + t.Run("case=create password identity", func(t *testing.T) { + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + i.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ + Identifiers: []string{subject}, + Config: sqlxx.JSONRawMessage(`{"hashed_password": "foo"}`), + }) + i.Traits = identity.Traits(`{"subject":"` + subject + `"}`) + + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) + }) - t.Run("case=create password identity", func(t *testing.T) { - i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) - i.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ - Identifiers: []string{subject}, + t.Run("case=should fail registration", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + _, body := makeRequest(t, "valid", action, url.Values{}) + snapshotx.SnapshotTJSON(t, body, snapshotx.ExceptPaths("expires_at", "updated_at", "issued_at", "id", "created_at", "ui.action", findCsrfTokenPath(t, body), "request_url"), snapshotx.ExceptNestedKeys("newLoginUrl", "new_login_url")) + }) + + t.Run("case=should fail registration id_first strategy enabled", func(t *testing.T) { + conf.Set(ctx, config.ViperKeySelfServiceLoginFlowStyle, "identifier_first") + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + _, body := makeRequest(t, "valid", action, url.Values{}) + snapshotx.SnapshotTJSON(t, body, snapshotx.ExceptPaths("expires_at", "updated_at", "issued_at", "id", "created_at", "ui.action", findCsrfTokenPath(t, body), "request_url"), snapshotx.ExceptNestedKeys("newLoginUrl", "new_login_url")) + }) + + t.Run("case=should fail login", func(t *testing.T) { + r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + _, body := makeRequest(t, "valid", action, url.Values{}) + snapshotx.SnapshotTJSON(t, body, snapshotx.ExceptPaths("expires_at", "updated_at", "issued_at", "id", "created_at", "ui.action", findCsrfTokenPath(t, body), "request_url"), snapshotx.ExceptNestedKeys("newLoginUrl", "new_login_url")) + }) }) - i.Traits = identity.Traits(`{"subject":"` + subject + `"}`) - require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) - }) + t.Run("case=should fail to register and return fresh login flow if email is already being used by oidc credentials", func(t *testing.T) { + subject = "email-exist-with-oidc-strategy-lh-" + strconv.FormatBool(loginHintsEnabled) + "@ory.sh" + scope = []string{"openid"} + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationLoginHints, strconv.FormatBool(loginHintsEnabled)) - t.Run("case=should fail registration", func(t *testing.T) { - r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) - action := assertFormValues(t, r.ID, "valid") - res, body := makeRequest(t, "valid", action, url.Values{}) - assertUIError(t, res, body, "An account with the same identifier (email, phone, username, ...) exists already.") - require.Contains(t, gjson.GetBytes(body, "ui.action").String(), "/self-service/login") - }) + t.Run("case=create oidc identity", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "secondProvider") + res, _ := makeRequest(t, "secondProvider", action, url.Values{}) + require.Equal(t, http.StatusOK, res.StatusCode) + }) - t.Run("case=should fail login", func(t *testing.T) { - r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) - action := assertFormValues(t, r.ID, "valid") - res, body := makeRequest(t, "valid", action, url.Values{}) - assertUIError(t, res, body, "An account with the same identifier (email, phone, username, ...) exists already.") + t.Run("case=should fail registration", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + _, body := makeRequest(t, "valid", action, url.Values{}) + snapshotx.SnapshotTJSON(t, body, snapshotx.ExceptPaths("expires_at", "updated_at", "issued_at", "id", "created_at", "ui.action", findCsrfTokenPath(t, body), "request_url"), snapshotx.ExceptNestedKeys("newLoginUrl", "new_login_url")) + }) + + t.Run("case=should fail registration id_first strategy enabled", func(t *testing.T) { + conf.Set(ctx, config.ViperKeySelfServiceLoginFlowStyle, "identifier_first") + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + _, body := makeRequest(t, "valid", action, url.Values{}) + snapshotx.SnapshotTJSON(t, body, snapshotx.ExceptPaths("expires_at", "updated_at", "issued_at", "id", "created_at", "ui.action", findCsrfTokenPath(t, body), "request_url"), snapshotx.ExceptNestedKeys("newLoginUrl", "new_login_url")) + }) + + t.Run("case=should fail login", func(t *testing.T) { + r := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + _, body := makeRequest(t, "valid", action, url.Values{}) + snapshotx.SnapshotTJSON(t, body, snapshotx.ExceptPaths("expires_at", "updated_at", "issued_at", "id", "created_at", "ui.action", findCsrfTokenPath(t, body), "request_url"), snapshotx.ExceptNestedKeys("newLoginUrl", "new_login_url")) + }) + }) }) - }) + } t.Run("case=should redirect to default return ts when sending authenticated login flow without forced flag", func(t *testing.T) { subject = "no-reauth-login@ory.sh" @@ -916,10 +1333,10 @@ func TestStrategy(t *testing.T) { fv := url.Values{"traits.name": {"valid-name"}} jar, _ := cookiejar.New(nil) r1 := newBrowserLoginFlow(t, returnTS.URL, time.Minute) - res1, body1 := makeRequestWithCookieJar(t, "valid", assertFormValues(t, r1.ID, "valid"), fv, jar) + res1, body1 := makeRequestWithCookieJar(t, "valid", assertFormValues(t, r1.ID, "valid"), fv, jar, nil) assertIdentity(t, res1, body1) r2 := newBrowserLoginFlow(t, returnTS.URL, time.Minute) - res2, body2 := makeRequestWithCookieJar(t, "valid", assertFormValues(t, r2.ID, "valid"), fv, jar) + res2, body2 := makeRequestWithCookieJar(t, "valid", assertFormValues(t, r2.ID, "valid"), fv, jar, nil) assertIdentity(t, res2, body2) assert.Equal(t, body1, body2) }) @@ -931,11 +1348,11 @@ func TestStrategy(t *testing.T) { fv := url.Values{"traits.name": {"valid-name"}} jar, _ := cookiejar.New(nil) r1 := newBrowserLoginFlow(t, returnTS.URL, time.Minute) - res1, body1 := makeRequestWithCookieJar(t, "valid", assertFormValues(t, r1.ID, "valid"), fv, jar) + res1, body1 := makeRequestWithCookieJar(t, "valid", assertFormValues(t, r1.ID, "valid"), fv, jar, nil) assertIdentity(t, res1, body1) r2 := newBrowserLoginFlow(t, returnTS.URL, time.Minute) require.NoError(t, reg.LoginFlowPersister().ForceLoginFlow(context.Background(), r2.ID)) - res2, body2 := makeRequestWithCookieJar(t, "valid", assertFormValues(t, r2.ID, "valid"), fv, jar) + res2, body2 := makeRequestWithCookieJar(t, "valid", assertFormValues(t, r2.ID, "valid"), fv, jar, nil) assertIdentity(t, res2, body2) assert.NotEqual(t, gjson.GetBytes(body1, "id"), gjson.GetBytes(body2, "id")) authAt1, err := time.Parse(time.RFC3339, gjson.GetBytes(body1, "authenticated_at").String()) @@ -1136,7 +1553,7 @@ func TestStrategy(t *testing.T) { require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i2)) }) - client := testhelpers.NewClientWithCookieJar(t, nil, false) + client := testhelpers.NewClientWithCookieJar(t, nil, nil) loginFlow := newLoginFlow(t, returnTS.URL, time.Minute, flow.TypeBrowser) var linkingLoginFlow struct { @@ -1151,9 +1568,10 @@ func TestStrategy(t *testing.T) { t.Run("step=should fail login and start a new flow", func(t *testing.T) { res, body := loginWithOIDC(t, client, loginFlow.ID, "valid") assert.True(t, res.Request.URL.Query().Has("no_org_ui")) - assertUIError(t, res, body, "You tried signing in with new-login-if-email-exist-with-password-strategy@ory.sh which is already in use by another account. You can sign in using your password.") - assert.Equal(t, "password", gjson.GetBytes(body, "ui.messages.#(id==4000028).context.available_credential_types.0").String()) - assert.Equal(t, "new-login-if-email-exist-with-password-strategy@ory.sh", gjson.GetBytes(body, "ui.messages.#(id==4000028).context.credential_identifier_hint").String()) + assertUIError(t, res, body, "You tried to sign in with \"new-login-if-email-exist-with-password-strategy@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"new-login-if-email-exist-with-password-strategy@ory.sh\" at \"generic\" as another way to sign in.") + assert.True(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==identifier)").Exists(), "%s", body) + assert.True(t, gjson.GetBytes(body, "ui.nodes.#(attributes.name==password)").Exists(), "%s", body) + assert.Equal(t, "new-login-if-email-exist-with-password-strategy@ory.sh", gjson.GetBytes(body, "ui.messages.#(id==1010016).context.duplicateIdentifier").String()) linkingLoginFlow.ID = gjson.GetBytes(body, "id").String() linkingLoginFlow.UIAction = gjson.GetBytes(body, "ui.action").String() linkingLoginFlow.CSRFToken = gjson.GetBytes(body, `ui.nodes.#(attributes.name=="csrf_token").attributes.value`).String() @@ -1216,18 +1634,21 @@ func TestStrategy(t *testing.T) { }) subject = email1 - client := testhelpers.NewClientWithCookieJar(t, nil, false) + client := testhelpers.NewClientWithCookieJar(t, nil, nil) loginFlow := newLoginFlow(t, returnTS.URL, time.Minute, flow.TypeBrowser) var linkingLoginFlow struct{ ID string } t.Run("step=should fail login and start a new login", func(t *testing.T) { - res, body := loginWithOIDC(t, client, loginFlow.ID, "valid") - assertUIError(t, res, body, "You tried signing in with existing-oidc-identity-1@ory.sh which is already in use by another account. You can sign in using social sign in. You can sign in using one of the following social sign in providers: Secondprovider.") + // To test the identifier mismatch + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationLoginHints, false) + res, body := loginWithOIDC(t, client, loginFlow.ID, "valid2") + assertUIError(t, res, body, "You tried to sign in with \"existing-oidc-identity-1@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"existing-oidc-identity-1@ory.sh\" at \"generic\" as another way to sign in.") linkingLoginFlow.ID = gjson.GetBytes(body, "id").String() assert.NotEqual(t, loginFlow.ID.String(), linkingLoginFlow.ID, "should have started a new flow") }) subject = email2 t.Run("step=should fail login if existing identity identifier doesn't match", func(t *testing.T) { + require.NotNil(t, linkingLoginFlow.ID) res, body := loginWithOIDC(t, client, uuid.Must(uuid.FromString(linkingLoginFlow.ID)), "valid") assertUIError(t, res, body, "Linked credentials do not match.") }) @@ -1249,16 +1670,6 @@ func TestStrategy(t *testing.T) { snapshotx.SnapshotTExcept(t, sr.UI, []string{"action", "nodes.0.attributes.value"}) }) - - t.Run("method=TestPopulateLoginMethod", func(t *testing.T) { - conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://foo/") - - sr, err := login.NewFlow(conf, time.Minute, "nosurf", &http.Request{URL: urlx.ParseOrPanic("/")}, flow.TypeBrowser) - require.NoError(t, err) - require.NoError(t, reg.LoginStrategies(context.Background()).MustStrategy(identity.CredentialsTypeOIDC).(*oidc.Strategy).PopulateLoginMethod(&http.Request{}, identity.AuthenticatorAssuranceLevel1, sr)) - - snapshotx.SnapshotTExcept(t, sr.UI, []string{"action", "nodes.0.attributes.value"}) - }) } func prettyJSON(t *testing.T, body []byte) string { @@ -1357,7 +1768,7 @@ func TestCountActiveFirstFactorCredentials(t *testing.T) { for _, v := range tc.in { in[v.Type] = v } - actual, err := strategy.CountActiveFirstFactorCredentials(in) + actual, err := strategy.CountActiveFirstFactorCredentials(context.Background(), in) require.NoError(t, err) assert.Equal(t, tc.expected, actual) }) @@ -1367,7 +1778,7 @@ func TestCountActiveFirstFactorCredentials(t *testing.T) { func TestDisabledEndpoint(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeOIDC.String(), false) - + ctx := context.Background() publicTS, _ := testhelpers.NewKratosServer(t, reg) t.Run("case=should not callback when oidc method is disabled", func(t *testing.T) { @@ -1385,7 +1796,7 @@ func TestDisabledEndpoint(t *testing.T) { t.Run("flow=settings", func(t *testing.T) { testhelpers.SetDefaultIdentitySchema(conf, "file://stub/stub.schema.json") - c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg) + c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg) f := testhelpers.InitializeSettingsFlowViaAPI(t, c, publicTS) res, err := c.PostForm(f.Ui.Action, url.Values{"link": {"oidc"}}) @@ -1457,3 +1868,12 @@ func TestPostEndpointRedirect(t *testing.T) { testhelpers.AssertNoCSRFCookieInResponse(t, publicTS, c, res) }) } + +func findCsrfTokenPath(t *testing.T, body []byte) string { + nodes := gjson.GetBytes(body, "ui.nodes").Array() + index := slices.IndexFunc(nodes, func(n gjson.Result) bool { + return n.Get("attributes.name").String() == "csrf_token" + }) + require.GreaterOrEqual(t, index, 0) + return fmt.Sprintf("ui.nodes.%v.attributes.value", index) +} diff --git a/selfservice/strategy/oidc/types.go b/selfservice/strategy/oidc/types.go index 4460c35ce67b..768e20d23531 100644 --- a/selfservice/strategy/oidc/types.go +++ b/selfservice/strategy/oidc/types.go @@ -18,10 +18,9 @@ type FlowMethod struct { *container.Container } -func AddProviders(c *container.Container, providers []Configuration, message func(provider string) *text.Message) { +func AddProviders(c *container.Container, providers []Configuration, message func(provider string, providerId string) *text.Message) { for _, p := range providers { - AddProvider(c, p.ID, message( - stringsx.Coalesce(p.Label, p.ID))) + AddProvider(c, p.ID, message(stringsx.Coalesce(p.Label, p.ID), p.ID)) } } diff --git a/selfservice/strategy/passkey/.schema/login.schema.json b/selfservice/strategy/passkey/.schema/login.schema.json new file mode 100644 index 000000000000..af01cb95d323 --- /dev/null +++ b/selfservice/strategy/passkey/.schema/login.schema.json @@ -0,0 +1,16 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/passkey/login.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "passkey_login": { + "type": "string" + }, + "method": { + "type": "string" + } + } +} diff --git a/selfservice/strategy/passkey/.schema/registration.schema.json b/selfservice/strategy/passkey/.schema/registration.schema.json new file mode 100644 index 000000000000..a2ac0441c0d0 --- /dev/null +++ b/selfservice/strategy/passkey/.schema/registration.schema.json @@ -0,0 +1,23 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/passkey/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "traits": { + "description": "This field will be overwritten in registration.go's decoder() method. Do not add anything to this field as it has no effect." + }, + "method": { + "type": "string" + }, + "passkey_register": { + "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + } + } +} diff --git a/selfservice/strategy/passkey/.schema/settings.schema.json b/selfservice/strategy/passkey/.schema/settings.schema.json new file mode 100644 index 000000000000..58bf997024fd --- /dev/null +++ b/selfservice/strategy/passkey/.schema/settings.schema.json @@ -0,0 +1,20 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/passkey/settings.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "method": { + "type": "string" + }, + "passkey_settings_register": { + "type": "string" + }, + "passkey_remove": { + "type": "string" + } + } +} + diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json new file mode 100644 index 000000000000..ffb5ec222642 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json @@ -0,0 +1,100 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "username webauthn", + "disabled": false, + "name": "identifier", + "node_type": "input", + "required": true, + "type": "text", + "value": "" + }, + "group": "default", + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_login_trigger", + "node_type": "input", + "onclick": "window.oryPasskeyLogin()", + "onclickTrigger": "oryPasskeyLogin", + "onload": "window.oryPasskeyLoginAutocompleteInit()", + "onloadTrigger": "oryPasskeyLoginAutocompleteInit", + "type": "button", + "value": "" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "id": 1010021, + "text": "Sign in with passkey", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_login", + "node_type": "input", + "onload": "window.oryPasskeyLoginAutocompleteInit()", + "onloadTrigger": "oryPasskeyLoginAutocompleteInit", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_challenge", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json new file mode 100644 index 000000000000..1e026fb9979a --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json @@ -0,0 +1,88 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "identifier", + "node_type": "input", + "type": "hidden", + "value": "foo@bar.com" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_login_trigger", + "node_type": "input", + "onclick": "window.oryPasskeyLogin()", + "onclickTrigger": "oryPasskeyLogin", + "type": "button", + "value": "" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "id": 1010021, + "text": "Sign in with passkey", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_login", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_challenge", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json new file mode 100644 index 000000000000..1e026fb9979a --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json @@ -0,0 +1,88 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "identifier", + "node_type": "input", + "type": "hidden", + "value": "foo@bar.com" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_login_trigger", + "node_type": "input", + "onclick": "window.oryPasskeyLogin()", + "onclickTrigger": "oryPasskeyLogin", + "type": "button", + "value": "" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "id": 1010021, + "text": "Sign in with passkey", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_login", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_challenge", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json new file mode 100644 index 000000000000..a0383567eda4 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json @@ -0,0 +1,123 @@ +[ + { + "attributes": { + "disabled": false, + "name": "passkey_register_trigger", + "node_type": "input", + "onclick": "window.oryPasskeySettingsRegistration()", + "onclickTrigger": "oryPasskeySettingsRegistration", + "type": "button", + "value": "" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "id": 1050019, + "text": "Add passkey", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_remove", + "node_type": "input", + "type": "submit", + "value": "626172626172" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "context": { + "added_at": "0001-01-01T00:00:00Z", + "added_at_unix": -62135596800, + "display_name": "bar" + }, + "id": 1050020, + "text": "Remove passkey \"bar\"", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_remove", + "node_type": "input", + "type": "submit", + "value": "666f6f666f6f" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "context": { + "added_at": "0001-01-01T00:00:00Z", + "added_at_unix": -62135596800, + "display_name": "foo" + }, + "id": 1050020, + "text": "Remove passkey \"foo\"", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_settings_register", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_create_data", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=browser-response.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=browser-response.json new file mode 100644 index 000000000000..fa4420351e94 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=browser-response.json @@ -0,0 +1,24 @@ +{ + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_remove", + "type": "submit", + "value": "666f6f666f6f", + "disabled": true, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050020, + "text": "Remove passkey \"foo\"", + "type": "info", + "context": { + "added_at": "0001-01-01T00:00:00Z", + "added_at_unix": -62135596800, + "display_name": "foo" + } + } + } +} diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=browser.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=browser.json new file mode 100644 index 000000000000..cccc13f86841 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=browser.json @@ -0,0 +1,8 @@ +{ + "passkey_register_trigger": [ + "" + ], + "passkey_remove": [ + "666f6f666f6f" + ] +} diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=spa-response.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=spa-response.json new file mode 100644 index 000000000000..fa4420351e94 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=spa-response.json @@ -0,0 +1,24 @@ +{ + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_remove", + "type": "submit", + "value": "666f6f666f6f", + "disabled": true, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1050020, + "text": "Remove passkey \"foo\"", + "type": "info", + "context": { + "added_at": "0001-01-01T00:00:00Z", + "added_at_unix": -62135596800, + "display_name": "foo" + } + } + } +} diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=spa.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=spa.json new file mode 100644 index 000000000000..cccc13f86841 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=fails_to_remove_passkey_if_it_is_the_last_credential_available-type=spa.json @@ -0,0 +1,8 @@ +{ + "passkey_register_trigger": [ + "" + ], + "passkey_remove": [ + "666f6f666f6f" + ] +} diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json new file mode 100644 index 000000000000..8d91edf04ce5 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json @@ -0,0 +1,75 @@ +[ + { + "attributes": { + "disabled": false, + "name": "passkey_register_trigger", + "node_type": "input", + "onclick": "window.oryPasskeySettingsRegistration()", + "onclickTrigger": "oryPasskeySettingsRegistration", + "type": "button", + "value": "" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "id": 1050019, + "text": "Add passkey", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_settings_register", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_create_data", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json new file mode 100644 index 000000000000..3e9aa5b5199e --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json @@ -0,0 +1,100 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "text", + "value": "", + "required": true, + "autocomplete": "username webauthn", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_challenge", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "script", + "group": "webauthn", + "attributes": { + "async": true, + "referrerpolicy": "no-referrer", + "crossorigin": "anonymous", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_login", + "type": "hidden", + "disabled": false, + "onload": "window.oryPasskeyLoginAutocompleteInit()", + "onloadTrigger": "oryPasskeyLoginAutocompleteInit", + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_login_trigger", + "type": "button", + "value": "", + "disabled": false, + "onclick": "window.oryPasskeyLogin()", + "onclickTrigger": "oryPasskeyLogin", + "onload": "window.oryPasskeyLoginAutocompleteInit()", + "onloadTrigger": "oryPasskeyLoginAutocompleteInit", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010021, + "text": "Sign in with passkey", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json new file mode 100644 index 000000000000..33d9f8afd952 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json @@ -0,0 +1,88 @@ +[ + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_challenge", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "script", + "group": "webauthn", + "attributes": { + "async": true, + "referrerpolicy": "no-referrer", + "crossorigin": "anonymous", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_login", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_login_trigger", + "type": "button", + "value": "", + "disabled": false, + "onclick": "window.oryPasskeyLogin()", + "onclickTrigger": "oryPasskeyLogin", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010021, + "text": "Sign in with passkey", + "type": "info" + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_disabled.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_disabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_enabled.json new file mode 100644 index 000000000000..94263a4da9d1 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_enabled.json @@ -0,0 +1,23 @@ +[ + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_login_trigger", + "type": "button", + "value": "", + "disabled": false, + "onclick": "window.oryPasskeyLogin()", + "onclickTrigger": "oryPasskeyLogin", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010021, + "text": "Sign in with passkey", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_passkey.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_passkey.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_passkey.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_passkey.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_passkey.json new file mode 100644 index 000000000000..94263a4da9d1 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_passkey.json @@ -0,0 +1,23 @@ +[ + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_login_trigger", + "type": "button", + "value": "", + "disabled": false, + "onclick": "window.oryPasskeyLogin()", + "onclickTrigger": "oryPasskeyLogin", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010021, + "text": "Sign in with passkey", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json new file mode 100644 index 000000000000..94263a4da9d1 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json @@ -0,0 +1,23 @@ +[ + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_login_trigger", + "type": "button", + "value": "", + "disabled": false, + "onclick": "window.oryPasskeyLogin()", + "onclickTrigger": "oryPasskeyLogin", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010021, + "text": "Sign in with passkey", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_disabled.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_disabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_enabled.json new file mode 100644 index 000000000000..94263a4da9d1 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_enabled.json @@ -0,0 +1,23 @@ +[ + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_login_trigger", + "type": "button", + "value": "", + "disabled": false, + "onclick": "window.oryPasskeyLogin()", + "onclickTrigger": "oryPasskeyLogin", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010021, + "text": "Sign in with passkey", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json new file mode 100644 index 000000000000..222443d4988b --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json @@ -0,0 +1,77 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "text", + "value": "", + "required": true, + "autocomplete": "username webauthn", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_challenge", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "script", + "group": "webauthn", + "attributes": { + "async": true, + "referrerpolicy": "no-referrer", + "crossorigin": "anonymous", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "passkey", + "attributes": { + "name": "passkey_login", + "type": "hidden", + "disabled": false, + "onload": "window.oryPasskeyLoginAutocompleteInit()", + "onloadTrigger": "oryPasskeyLoginAutocompleteInit", + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-browser.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-browser.json new file mode 100644 index 000000000000..cd42a6256ce0 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-browser.json @@ -0,0 +1,80 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.foobar", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "password", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "new-password", + "disabled": false, + "name": "password", + "node_type": "input", + "required": true, + "type": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.username", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "password", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-spa.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-spa.json new file mode 100644 index 000000000000..cd42a6256ce0 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_does_not_exist_when_passwordless_is_disabled-spa.json @@ -0,0 +1,80 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.foobar", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "password", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "new-password", + "disabled": false, + "name": "password", + "node_type": "input", + "required": true, + "type": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.username", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "password", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json new file mode 100644 index 000000000000..e4c5160c9697 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json @@ -0,0 +1,139 @@ +[ + { + "attributes": { + "disabled": false, + "name": "traits.foobar", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.username", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_register", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_register_trigger", + "node_type": "input", + "onclick": "window.oryPasskeyRegistration()", + "onclickTrigger": "oryPasskeyRegistration", + "type": "button" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "id": 1040007, + "text": "Sign up with passkey", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_create_data", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "new-password", + "disabled": false, + "name": "password", + "node_type": "input", + "required": true, + "type": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json new file mode 100644 index 000000000000..e4c5160c9697 --- /dev/null +++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json @@ -0,0 +1,139 @@ +[ + { + "attributes": { + "disabled": false, + "name": "traits.foobar", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "traits.username", + "node_type": "input", + "required": true, + "type": "text" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_register", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_register_trigger", + "node_type": "input", + "onclick": "window.oryPasskeyRegistration()", + "onclickTrigger": "oryPasskeyRegistration", + "type": "button" + }, + "group": "passkey", + "messages": [], + "meta": { + "label": { + "id": 1040007, + "text": "Sign up with passkey", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "passkey_create_data", + "node_type": "input", + "type": "hidden" + }, + "group": "passkey", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "autocomplete": "new-password", + "disabled": false, + "name": "password", + "node_type": "input", + "required": true, + "type": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + }, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "method", + "node_type": "input", + "type": "submit", + "value": "password" + }, + "group": "password", + "messages": [], + "meta": { + "label": { + "id": 1040001, + "text": "Sign up", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/passkey/fixtures/login/success/credentials.json b/selfservice/strategy/passkey/fixtures/login/success/credentials.json new file mode 100644 index 000000000000..1fdf7ca53fb8 --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/login/success/credentials.json @@ -0,0 +1,18 @@ +{ + "credentials": [ + { + "id": "OE7fnoAeqaydiBM4+fQsbYMiO+EObq97WYb/c1HuX8Crbsb2777xs+upv7muXE8hOLkm6lQHC1ahegnzw+aIsQ==", + "public_key": "pQECAyYgASFYIPW2FsD6d/Lc7SU33hMhJUxafOA3JWpsLka8eKO+OPRkIlggkkPt8ocrupQOuvy+8HbQLSLiu899EdchJlWdMPE1tiw=", + "attestation_type": "none", + "authenticator": { + "aaguid": "AAAAAAAAAAAAAAAAAAAAAA==", + "sign_count": 3, + "clone_warning": false + }, + "display_name": "some-key", + "added_at": "2021-08-17T10:18:55Z", + "is_passwordless": true + } + ], + "user_handle": "c29tZS1yYW5kb20tdXNlci1oYW5kbGU=" +} diff --git a/selfservice/strategy/passkey/fixtures/login/success/identity.json b/selfservice/strategy/passkey/fixtures/login/success/identity.json new file mode 100644 index 000000000000..2a6ad6975599 --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/login/success/identity.json @@ -0,0 +1,13 @@ +{ + "id": "f5d1b6a3-a4bb-44f7-9161-f4f877efe9ad", + "schema_id": "default", + "schema_url": "http://localhost:4455/schemas/default", + "state": "active", + "state_changed_at": "2021-08-17T12:18:01.690425+02:00", + "traits": { + "email": "foo@bar.com", + "website": "https://www.ory.sh" + }, + "created_at": "2021-08-17T12:18:01.690741+02:00", + "updated_at": "2021-08-17T12:18:01.690741+02:00" +} diff --git a/selfservice/strategy/passkey/fixtures/login/success/internal_context.json b/selfservice/strategy/passkey/fixtures/login/success/internal_context.json new file mode 100644 index 000000000000..fcbca8822781 --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/login/success/internal_context.json @@ -0,0 +1,10 @@ +{ + "passkey_session_data": { + "challenge": "WzZCWULmaq5xTAlA0YYHlqoubqAhe1AWdLRZCIBAMcM", + "user_id": "", + "allowed_credentials": [ + "OE7fnoAeqaydiBM4+fQsbYMiO+EObq97WYb/c1HuX8Crbsb2777xs+upv7muXE8hOLkm6lQHC1ahegnzw+aIsQ==" + ], + "userVerification": "" + } +} diff --git a/selfservice/strategy/passkey/fixtures/login/success/response.json b/selfservice/strategy/passkey/fixtures/login/success/response.json new file mode 100644 index 000000000000..3a4ff0ad5ac2 --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/login/success/response.json @@ -0,0 +1,11 @@ +{ + "id": "OE7fnoAeqaydiBM4-fQsbYMiO-EObq97WYb_c1HuX8Crbsb2777xs-upv7muXE8hOLkm6lQHC1ahegnzw-aIsQ", + "rawId": "OE7fnoAeqaydiBM4-fQsbYMiO-EObq97WYb_c1HuX8Crbsb2777xs-upv7muXE8hOLkm6lQHC1ahegnzw-aIsQ", + "type": "public-key", + "response": { + "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAACg", + "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiV3paQ1dVTG1hcTV4VEFsQTBZWUhscW91YnFBaGUxQVdkTFJaQ0lCQU1jTSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDQ1NSIsImNyb3NzT3JpZ2luIjpmYWxzZX0", + "signature": "MEQCIHtRzzmLJrTPucNIRpPkstxR8oGJEzrm558LFe2jHTesAiAy2SGuBMDkdVdMJU4WJR2qFSpbAHQUvwG--Gv3vK8vDA", + "userHandle": "c29tZS1yYW5kb20tdXNlci1oYW5kbGU=" + } +} diff --git a/selfservice/strategy/passkey/fixtures/registration/failure/internal_context_missing_user_id.json b/selfservice/strategy/passkey/fixtures/registration/failure/internal_context_missing_user_id.json new file mode 100644 index 000000000000..2e83f5ae0a8a --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/registration/failure/internal_context_missing_user_id.json @@ -0,0 +1,7 @@ +{ + "passkey_session_data": { + "challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY", + "user_id": "", + "userVerification": "" + } +} diff --git a/selfservice/strategy/passkey/fixtures/registration/failure/internal_context_wrong_user_id.json b/selfservice/strategy/passkey/fixtures/registration/failure/internal_context_wrong_user_id.json new file mode 100644 index 000000000000..50d686c1ed30 --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/registration/failure/internal_context_wrong_user_id.json @@ -0,0 +1,7 @@ +{ + "passkey_session_data": { + "challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY", + "user_id": "wrong", + "userVerification": "" + } +} diff --git a/selfservice/strategy/passkey/fixtures/registration/success/android/internal_context.json b/selfservice/strategy/passkey/fixtures/registration/success/android/internal_context.json new file mode 100644 index 000000000000..cd9949ce0093 --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/registration/success/android/internal_context.json @@ -0,0 +1,7 @@ +{ + "passkey_session_data": { + "challenge": "mFtAwmtDDdwcO6200I2H6oWjzOiF21lZhQVlrC4tdaU", + "user_id": "d29OeDNJVjdYR2NRa09RVHhNVG1ZbHE1ejBDYzM1dGV3UWxFT25yaUJKcTUyb0VOR0pUMk5PeXExRXp3Z2M2dg", + "userVerification": "" + } +} diff --git a/selfservice/strategy/passkey/fixtures/registration/success/android/response.json b/selfservice/strategy/passkey/fixtures/registration/success/android/response.json new file mode 100644 index 000000000000..8b480b356790 --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/registration/success/android/response.json @@ -0,0 +1,9 @@ +{ + "id": "mK2RV0b2NUGDsj8QqH0XtQ", + "rawId": "mK2RV0b2NUGDsj8QqH0XtQ", + "response": { + "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViUJYVxRmHaAcJuz7n2X5FJILFPwxIhVpoURyBRglMxnFpdAAAAAOqbjWZNAR0hPOS2tIy1ddQAEJitkVdG9jVBg7I_EKh9F7WlAQIDJiABIVggjEkfDDjIm8yAYfth4u0EV7ApX4kclQONhpK5BLc7W6wiWCCHiHhRNqf8Qhc7bjoIFTqw4lafiC7yrXvojU_WMNcutA", + "clientDataJson": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoibUZ0QXdtdEREZHdjTzYyMDBJMkg2b1dqek9pRjIxbFpoUVZsckM0dGRhVSIsIm9yaWdpbiI6ImFuZHJvaWQ6YXBrLWtleS1oYXNoOlMyUmZOWWdKbVFpS2dkNi1zZGJqVzdwaGNMX09UUDR2R0U4TDUxUTJHQjAiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20udHJwLmFuZC5wZXJzb25hbC5xbCJ9" + }, + "type": "public-key" +} diff --git a/selfservice/strategy/passkey/fixtures/registration/success/browser/identity.json b/selfservice/strategy/passkey/fixtures/registration/success/browser/identity.json new file mode 100644 index 000000000000..5aa6d11cae1c --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/registration/success/browser/identity.json @@ -0,0 +1,14 @@ +{ + "id": "6e11a9a7-62fd-4c88-871a-097f18f0306f", + "schema_id": "default", + "schema_url": "http://localhost:4455/schemas/default", + "state": "active", + "state_changed_at": "2021-08-17T11:15:59.232051+02:00", + "traits": { + "email": "foo@bar.com", + "website": "https://www.ory.sh" + }, + "created_at": "2021-08-17T11:15:59.232288+02:00", + "updated_at": "2021-08-17T11:15:59.232288+02:00" +} + diff --git a/selfservice/strategy/passkey/fixtures/registration/success/browser/internal_context.json b/selfservice/strategy/passkey/fixtures/registration/success/browser/internal_context.json new file mode 100644 index 000000000000..c4c2e93e7b8e --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/registration/success/browser/internal_context.json @@ -0,0 +1,7 @@ +{ + "passkey_session_data": { + "challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY", + "user_id": "bhGpp2L9TIiHGgl/GPAwbw==", + "userVerification": "" + } +} diff --git a/selfservice/strategy/passkey/fixtures/registration/success/browser/response.json b/selfservice/strategy/passkey/fixtures/registration/success/browser/response.json new file mode 100644 index 000000000000..7d2b3819927d --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/registration/success/browser/response.json @@ -0,0 +1,9 @@ +{ + "id": "L1yOrxHy5Lq72lAPahaWdl0q9gsXRzV2BJ4xJmkTVH_8uuVKU-FVbJlVRwYGzPNc1IjCWUYAK0H0YSpd5hz-Pg", + "rawId": "L1yOrxHy5Lq72lAPahaWdl0q9gsXRzV2BJ4xJmkTVH_8uuVKU-FVbJlVRwYGzPNc1IjCWUYAK0H0YSpd5hz-Pg", + "type": "public-key", + "response": { + "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAABAAAAAAAAAAAAAAAAAAAAAAAQC9cjq8R8uS6u9pQD2oWlnZdKvYLF0c1dgSeMSZpE1R__LrlSlPhVWyZVUcGBszzXNSIwllGACtB9GEqXeYc_j6lAQIDJiABIVggFFzdor6hBMgrpYLCds8Uu2JtPaaaxKU6LEAUT6QRZ5UiWCA24TI4vED6rrTUjykchoAln67u5GT1nwmzjvrk79HhlQ", + "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiVWx4SFNUa3VNdnRWRG9WOXk1bGh1OU95TlVQOFA3TVAwUllBVDZJbV9yWSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDQ1NSIsImNyb3NzT3JpZ2luIjpmYWxzZX0" + } +} diff --git a/selfservice/strategy/passkey/fixtures/settings/success/identity.json b/selfservice/strategy/passkey/fixtures/settings/success/identity.json new file mode 100644 index 000000000000..5aa6d11cae1c --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/settings/success/identity.json @@ -0,0 +1,14 @@ +{ + "id": "6e11a9a7-62fd-4c88-871a-097f18f0306f", + "schema_id": "default", + "schema_url": "http://localhost:4455/schemas/default", + "state": "active", + "state_changed_at": "2021-08-17T11:15:59.232051+02:00", + "traits": { + "email": "foo@bar.com", + "website": "https://www.ory.sh" + }, + "created_at": "2021-08-17T11:15:59.232288+02:00", + "updated_at": "2021-08-17T11:15:59.232288+02:00" +} + diff --git a/selfservice/strategy/passkey/fixtures/settings/success/internal_context.json b/selfservice/strategy/passkey/fixtures/settings/success/internal_context.json new file mode 100644 index 000000000000..c4c2e93e7b8e --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/settings/success/internal_context.json @@ -0,0 +1,7 @@ +{ + "passkey_session_data": { + "challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY", + "user_id": "bhGpp2L9TIiHGgl/GPAwbw==", + "userVerification": "" + } +} diff --git a/selfservice/strategy/passkey/fixtures/settings/success/response.json b/selfservice/strategy/passkey/fixtures/settings/success/response.json new file mode 100644 index 000000000000..7d2b3819927d --- /dev/null +++ b/selfservice/strategy/passkey/fixtures/settings/success/response.json @@ -0,0 +1,9 @@ +{ + "id": "L1yOrxHy5Lq72lAPahaWdl0q9gsXRzV2BJ4xJmkTVH_8uuVKU-FVbJlVRwYGzPNc1IjCWUYAK0H0YSpd5hz-Pg", + "rawId": "L1yOrxHy5Lq72lAPahaWdl0q9gsXRzV2BJ4xJmkTVH_8uuVKU-FVbJlVRwYGzPNc1IjCWUYAK0H0YSpd5hz-Pg", + "type": "public-key", + "response": { + "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAABAAAAAAAAAAAAAAAAAAAAAAAQC9cjq8R8uS6u9pQD2oWlnZdKvYLF0c1dgSeMSZpE1R__LrlSlPhVWyZVUcGBszzXNSIwllGACtB9GEqXeYc_j6lAQIDJiABIVggFFzdor6hBMgrpYLCds8Uu2JtPaaaxKU6LEAUT6QRZ5UiWCA24TI4vED6rrTUjykchoAln67u5GT1nwmzjvrk79HhlQ", + "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiVWx4SFNUa3VNdnRWRG9WOXk1bGh1OU95TlVQOFA3TVAwUllBVDZJbV9yWSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDQ1NSIsImNyb3NzT3JpZ2luIjpmYWxzZX0" + } +} diff --git a/selfservice/strategy/passkey/passkey_login.go b/selfservice/strategy/passkey/passkey_login.go new file mode 100644 index 000000000000..6678248b3e3b --- /dev/null +++ b/selfservice/strategy/passkey/passkey_login.go @@ -0,0 +1,488 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey + +import ( + "context" + _ "embed" + "encoding/json" + "net/http" + "strings" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/x/otelx" + + "github.com/ory/kratos/selfservice/strategy/idfirst" + + "github.com/ory/kratos/x/webauthnx/js" + + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/herodot" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flowhelpers" + "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/webauthnx" + "github.com/ory/x/decoderx" +) + +var _ login.FormHydrator = new(Strategy) + +func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) { + webauthnx.RegisterWebauthnRoute(r) +} + +func (s *Strategy) populateLoginMethodForPasskeys(r *http.Request, loginFlow *login.Flow) error { + ctx := r.Context() + + loginFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) + if err != nil { + return err + } + + webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) + if err != nil { + return errors.WithStack(err) + } + option, sessionData, err := webAuthn.BeginDiscoverableLogin() + if err != nil { + return errors.WithStack(err) + } + + loginFlow.InternalContext, err = sjson.SetBytes( + loginFlow.InternalContext, + flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), + sessionData, + ) + if err != nil { + return errors.WithStack(err) + } + + injectWebAuthnOptions, err := json.Marshal(option) + if err != nil { + return errors.WithStack(err) + } + + loginFlow.UI.Nodes.Upsert(node.NewInputField( + "identifier", + "", + node.DefaultGroup, + node.InputAttributeTypeText, + node.WithRequiredInputAttribute, + func(attributes *node.InputAttributes) { attributes.Autocomplete = "username webauthn" }, + ).WithMetaLabel(identifierLabel)) + + loginFlow.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyChallenge, + Type: node.InputAttributeTypeHidden, + FieldValue: string(injectWebAuthnOptions), + }, + }) + + loginFlow.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + + loginFlow.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyLogin, + Type: node.InputAttributeTypeHidden, + OnLoad: js.WebAuthnTriggersPasskeyLoginAutocompleteInit.String() + "()", + OnLoadTrigger: js.WebAuthnTriggersPasskeyLoginAutocompleteInit, + }, + }) + + return nil +} + +func (s *Strategy) handleLoginError(r *http.Request, f *login.Flow, err error) error { + if f != nil { + f.UI.Nodes.ResetNodes(node.PasskeyLogin) + if f.Type == flow.TypeBrowser { + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + } + + return err +} + +// Update Login Flow with Passkey Method +// +// swagger:model updateLoginFlowWithPasskeyMethod +type updateLoginFlowWithPasskeyMethod struct { + // Method should be set to "passkey" when logging in using the Passkey strategy. + // + // required: true + Method string `json:"method"` + + // Sending the anti-csrf token is only required for browser login flows. + CSRFToken string `json:"csrf_token"` + + // Login a WebAuthn Security Key + // + // This must contain the ID of the WebAuthN connection. + Login string `json:"passkey_login"` +} + +func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, _ *session.Session) (i *identity.Identity, err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.passkey.strategy.Login") + defer otelx.End(span, &err) + + if f.Type != flow.TypeBrowser { + span.SetAttributes(attribute.String("not_responsible_reason", "flow type is not browser")) + return nil, flow.ErrStrategyNotResponsible + } + + var p updateLoginFlowWithPasskeyMethod + if err := s.hd.Decode(r, &p, + decoderx.HTTPKeepRequestBody(true), + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.MustHTTPRawJSONSchemaCompiler(loginSchema), + decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { + return nil, s.handleLoginError(r, f, err) + } + + if len(p.Login) > 0 || p.Method == s.SettingsStrategyID() { + // This method has only two submit buttons + p.Method = s.SettingsStrategyID() + } else { + span.SetAttributes(attribute.String("not_responsible_reason", "no login value and mismatched method")) + return nil, flow.ErrStrategyNotResponsible + } + + if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + return nil, s.handleLoginError(r, f, err) + } + + if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + return nil, s.handleLoginError(r, f, err) + } + + return s.loginPasswordless(ctx, w, r, f, &p) +} + +func (s *Strategy) loginPasswordless(ctx context.Context, w http.ResponseWriter, r *http.Request, f *login.Flow, p *updateLoginFlowWithPasskeyMethod) (i *identity.Identity, err error) { + if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil { + trace.SpanFromContext(ctx).SetAttributes(attribute.String("not_responsible_reason", "requested AAL is not AAL1")) + return nil, s.handleLoginError(r, f, err) + } + + if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + return nil, s.handleLoginError(r, f, err) + } + + if len(p.Login) == 0 { + // Reset all nodes to not confuse users. + f.UI.Nodes = node.Nodes{} + + if err := s.populateLoginMethodForPasskeys(r, f); err != nil { + return nil, s.handleLoginError(r, f, err) + } + + redirectTo := f.AppendTo(s.d.Config().SelfServiceFlowLoginUI(ctx)).String() + if x.IsJSONRequest(r) { + s.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(redirectTo)) + } else { + http.Redirect(w, r, redirectTo, http.StatusSeeOther) + } + + return nil, errors.WithStack(flow.ErrCompletedByStrategy) + } + + return s.loginAuthenticate(ctx, r, f, p, identity.AuthenticatorAssuranceLevel1) +} + +func (s *Strategy) loginAuthenticate(ctx context.Context, r *http.Request, f *login.Flow, p *updateLoginFlowWithPasskeyMethod, _ identity.AuthenticatorAssuranceLevel) (*identity.Identity, error) { + web, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) + if err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error()))) + } + + webAuthnResponse, err := protocol.ParseCredentialRequestResponseBody(strings.NewReader(p.Login)) + if err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to parse WebAuthn response.").WithDebug(err.Error()))) + } + + var webAuthnSess webauthn.SessionData + if err := json.Unmarshal([]byte(gjson.GetBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)).Raw), &webAuthnSess); err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError. + WithReasonf("Expected WebAuthN in internal context to be an object but got: %s", err))) + } + webAuthnSess.UserID = nil + + userHandle := webAuthnResponse.Response.UserHandle + credentialType := identity.CredentialsTypePasskey + i, _, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, identity.CredentialsTypePasskey, string(userHandle)) + if err != nil { + // Migration strategy: Don't give up yet! If we don't find a "passkey" credential + // here, look for a "webauthn" credential next + if i, err = s.d.PrivilegedIdentityPool().FindIdentityByWebauthnUserHandle(ctx, userHandle); err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewNoWebAuthnCredentials())) + } + credentialType = identity.CredentialsTypeWebAuthn + } + err = s.d.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, i, identity.ExpandCredentials) + if err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError. + WithReason("Could not load identity credentials"). + WithWrap(err))) + } + + c, ok := i.GetCredentials(credentialType) + if !ok { + return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewNoWebAuthnRegistered())) + } + + var o identity.CredentialsWebAuthnConfig + if err := json.Unmarshal(c.Config, &o); err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError. + WithReason("The WebAuthn credentials could not be decoded properly"). + WithDebug(err.Error()). + WithWrap(err))) + } + + webAuthCreds := o.Credentials.PasswordlessOnly(&webAuthnResponse.Response.AuthenticatorData.Flags) + _, err = web.ValidateDiscoverableLogin( + func(rawID, userHandle []byte) (user webauthn.User, err error) { + return webauthnx.NewUser(userHandle, webAuthCreds, web.Config), nil + }, webAuthnSess, webAuthnResponse) + if err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewWebAuthnVerifierWrongError("#/"))) + } + + // Remove the WebAuthn URL from the internal context now that it is set! + f.InternalContext, err = sjson.DeleteBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) + if err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(err)) + } + + f.Active = s.ID() + if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error()))) + } + + return i, nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, f *login.Flow) error { + if f.Type != flow.TypeBrowser { + return nil + } + + ctx := r.Context() + + identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, f, s.ID()) + if identifier == "" { + return nil + } + + id, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, id.ID) + if err != nil { + return err + } + + cred, ok := id.GetCredentials(s.ID()) + if !ok { + // Identity has no passkey + return nil + } + + var conf identity.CredentialsWebAuthnConfig + if err := json.Unmarshal(cred.Config, &conf); err != nil { + return errors.WithStack(err) + } + + webAuthCreds := conf.Credentials.ToWebAuthn() + if len(webAuthCreds) == 0 { + // Identity has no webauthn + return nil + } + + passkeyIdentifier := s.PasskeyDisplayNameFromIdentity(ctx, id) + + webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) + if err != nil { + return errors.WithStack(err) + } + option, sessionData, err := webAuthn.BeginLogin(&webauthnx.User{ + Name: passkeyIdentifier, + ID: conf.UserHandle, + Credentials: webAuthCreds, + Config: webAuthn.Config, + }) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initiate passkey login.").WithDebug(err.Error())) + } + + f.InternalContext, err = sjson.SetBytes( + f.InternalContext, + flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), + sessionData, + ) + if err != nil { + return errors.WithStack(err) + } + + injectWebAuthnOptions, err := json.Marshal(option) + if err != nil { + return errors.WithStack(err) + } + + f.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyChallenge, + Type: node.InputAttributeTypeHidden, + FieldValue: string(injectWebAuthnOptions), + }, + }) + + f.UI.Nodes.Append(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + + f.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyLogin, + Type: node.InputAttributeTypeHidden, + }, + }) + + f.UI.Nodes.Append(node.NewInputField( + node.PasskeyLoginTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(attr *node.InputAttributes) { + //nolint:staticcheck + attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js + attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin + }), + ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + f.UI.SetNode(node.NewInputField( + "identifier", + passkeyIdentifier, + node.DefaultGroup, + node.InputAttributeTypeHidden, + )) + + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, f *login.Flow) error { + if f.Type != flow.TypeBrowser { + return nil + } + + if err := s.populateLoginMethodForPasskeys(r, f); err != nil { + return err + } + + f.UI.Nodes.Append(node.NewInputField( + node.PasskeyLoginTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(attr *node.InputAttributes) { + //nolint:staticcheck + attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js + attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin + + //nolint:staticcheck + attr.OnLoad = js.WebAuthnTriggersPasskeyLoginAutocompleteInit.String() + "()" // same here + attr.OnLoadTrigger = js.WebAuthnTriggersPasskeyLoginAutocompleteInit + }), + ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) + + return nil +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, sr *login.Flow, opts ...login.FormHydratorModifier) error { + if sr.Type != flow.TypeBrowser { + return errors.WithStack(idfirst.ErrNoCredentialsFound) + } + + ctx := r.Context() + o := login.NewFormHydratorOptions(opts) + + var count int + if o.IdentityHint != nil { + var err error + // If we have an identity hint we can perform identity credentials discovery and + // hide this credential if it should not be included. + count, err = s.CountActiveFirstFactorCredentials(ctx, o.IdentityHint.Credentials) + if err != nil { + return err + } + } + + if count > 0 || s.d.Config().SecurityAccountEnumerationMitigate(ctx) { + sr.UI.Nodes.Append(node.NewInputField( + node.PasskeyLoginTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(attr *node.InputAttributes) { + //nolint:staticcheck + attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js + attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin + }), + ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey())) + } + + if count == 0 { + return errors.WithStack(idfirst.ErrNoCredentialsFound) + } + + return nil +} + +func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, sr *login.Flow) error { + if sr.Type != flow.TypeBrowser { + return nil + } + + if err := s.populateLoginMethodForPasskeys(r, sr); err != nil { + return err + } + + return nil +} diff --git a/selfservice/strategy/passkey/passkey_login_test.go b/selfservice/strategy/passkey/passkey_login_test.go new file mode 100644 index 000000000000..028d7281c5e6 --- /dev/null +++ b/selfservice/strategy/passkey/passkey_login_test.go @@ -0,0 +1,471 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey_test + +import ( + "context" + _ "embed" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/ory/kratos/selfservice/strategy/idfirst" + + configtesthelpers "github.com/ory/kratos/driver/config/testhelpers" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/strategy/passkey" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/snapshotx" +) + +var ( + //go:embed fixtures/login/success/identity.json + loginSuccessIdentity []byte + //go:embed fixtures/login/success/credentials.json + loginPasswordlessCredentials []byte + //go:embed fixtures/login/success/internal_context.json + loginPasswordlessContext []byte + //go:embed fixtures/login/success/response.json + loginPasswordlessResponse []byte +) + +func TestPopulateLoginMethod(t *testing.T) { + t.Parallel() + fix := newLoginFixture(t) + s := passkey.NewStrategy(fix.reg) + + t.Run("case=should not handle AAL2", func(t *testing.T) { + loginFlow := &login.Flow{Type: flow.TypeBrowser} + assert.Nil(t, s.PopulateLoginMethodSecondFactor(nil, loginFlow)) + }) + + t.Run("case=should not handle API flows", func(t *testing.T) { + loginFlow := &login.Flow{Type: flow.TypeAPI} + assert.Nil(t, s.PopulateLoginMethodFirstFactor(nil, loginFlow)) + }) +} + +func TestCompleteLogin(t *testing.T) { + t.Parallel() + fix := newLoginFixture(t) + + t.Run("case=should return webauthn.js", func(t *testing.T) { + res, err := fix.publicTS.Client().Get(fix.publicTS.URL + "/.well-known/ory/webauthn.js") + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, "text/javascript; charset=UTF-8", res.Header.Get("Content-Type")) + }) + + t.Run("flow=passwordless", func(t *testing.T) { + t.Run("case=passkey button exists", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, client, fix.publicTS, false, true, false, false) + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "0.attributes.value", + "2.attributes.nonce", + "2.attributes.src", + "5.attributes.value", + }) + }) + + t.Run("case=passkey shows error if user tries to sign in but no such user exists", func(t *testing.T) { + payload := func(v url.Values) { + v.Set("method", "passkey") + v.Set(node.PasskeyLogin, string(loginPasswordlessResponse)) + } + + check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) { + fix.checkURL(t, shouldRedirect, res) + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Equal(t, text.NewErrorValidationSuchNoWebAuthnUser().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + } + + t.Run("type=browser", func(t *testing.T) { + body, res := fix.loginViaBrowser(t, false, payload, testhelpers.NewClientWithCookies(t)) + check(t, true, body, res) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := fix.loginViaBrowser(t, true, payload, testhelpers.NewClientWithCookies(t)) + check(t, false, body, res) + }) + }) + + t.Run("case=should fail if passkey login is invalid", func(t *testing.T) { + payload := func(v url.Values) { + v.Set("method", "passkey") + v.Set("passkey_login", "invalid passkey data") + } + + check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) { + fix.checkURL(t, shouldRedirect, res) + assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) + assert.Equal(t, "Unable to parse WebAuthn response.", gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + } + + t.Run("type=browser", func(t *testing.T) { + body, res := fix.loginViaBrowser(t, false, payload, testhelpers.NewClientWithCookies(t)) + check(t, true, body, res) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := fix.loginViaBrowser(t, true, payload, testhelpers.NewClientWithCookies(t)) + check(t, false, body, res) + }) + }) + + t.Run("case=should fail if passkey login is empty", func(t *testing.T) { + payload := func(v url.Values) { + v.Set("method", "passkey") + } + + t.Run("type=browser", func(t *testing.T) { + _, res := fix.loginViaBrowser(t, false, payload, testhelpers.NewClientWithCookies(t)) + fix.checkURL(t, true, res) + }) + + t.Run("type=spa", func(t *testing.T) { + body, res := fix.loginViaBrowser(t, true, payload, testhelpers.NewClientWithCookies(t)) + fix.checkURL(t, false, res) + assert.Equal(t, "browser_location_change_required", gjson.Get(body, "error.id").String(), "%s", body) + }) + }) + + t.Run("case=fails with invalid internal state", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + fix.conf.MustSet(fix.ctx, config.ViperKeySessionWhoAmIAAL, "aal1") + // We load our identity which we will use to replay the webauth session + fix.createIdentityWithPasskey(t, identity.Credentials{ + Config: loginPasswordlessCredentials, + Version: 1, + }) + + browserClient := testhelpers.NewClientWithCookies(t) + body, _, _ := fix.submitWebAuthnLoginWithClient(t, spa, []byte("invalid context"), browserClient, func(values url.Values) { + values.Set(node.PasskeyLogin, string(loginPasswordlessResponse)) + }, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel1)) + + if spa { + assert.Equal( + t, + "Expected WebAuthN in internal context to be an object but got: unexpected end of JSON input", + gjson.Get(body, "error.reason").String(), + "%s", body, + ) + } else { + assert.Equal( + t, + "Expected WebAuthN in internal context to be an object but got: unexpected end of JSON input", + gjson.Get(body, "reason").String(), + "%s", body, + ) + } + } + + t.Run("type=browser", func(t *testing.T) { + run(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true) + }) + }) + + t.Run("case=succeeds with passwordless login", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + fix.conf.MustSet(fix.ctx, config.ViperKeySessionWhoAmIAAL, "aal1") + // We load our identity which we will use to replay the webauth session + id := fix.createIdentityWithPasskey(t, identity.Credentials{ + Config: loginPasswordlessCredentials, + Version: 1, + }) + + browserClient := testhelpers.NewClientWithCookies(t) + body, res, f := fix.submitWebAuthnLoginWithClient(t, spa, loginPasswordlessContext, browserClient, func(values url.Values) { + values.Set(node.PasskeyLogin, string(loginPasswordlessResponse)) + }, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel1)) + + prefix := "" + if spa { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+login.RouteSubmitFlow) + prefix = "session." + } else { + assert.Contains(t, res.Request.URL.String(), fix.redirTS.URL) + } + + assert.True(t, gjson.Get(body, prefix+"active").Bool(), "%s", body) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, gjson.Get(body, prefix+"authenticator_assurance_level").String(), "%s", body) + assert.EqualValues(t, identity.CredentialsTypePasskey, gjson.Get(body, prefix+"authentication_methods.#(method==passkey).method").String(), "%s", body) + assert.EqualValues(t, id.ID.String(), gjson.Get(body, prefix+"identity.id").String(), "%s", body) + + actualFlow, err := fix.reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + + assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypePasskey, passkey.InternalContextKeySessionData))) + if spa { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), fix.conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", body) + } else { + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) + } + } + + // We test here that login works even if the identity schema contains + // { webauthn: { identifier: true } } instead of + // { passkey: { display_name: true } } + t.Run("webauthn_identifier", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/login_webauthn.schema.json") + t.Run("type=browser", func(t *testing.T) { run(t, false) }) + t.Run("type=spa", func(t *testing.T) { run(t, true) }) + }) + t.Run("passkey_display_name", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/login.schema.json") + t.Run("type=browser", func(t *testing.T) { run(t, false) }) + t.Run("type=spa", func(t *testing.T) { run(t, true) }) + }) + }) + }) + + t.Run("flow=refresh", func(t *testing.T) { + fix := newLoginFixture(t) + fix.conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, "aal1") + loginFixtureSuccessEmail := gjson.GetBytes(loginSuccessIdentity, "traits.email").String() + + run := func(t *testing.T, ctx context.Context, id *identity.Identity, context, response []byte, isSPA bool, expectedAAL identity.AuthenticatorAssuranceLevel) { + body, res, f := fix.submitWebAuthnLogin(t, ctx, isSPA, id, context, func(values url.Values) { + values.Set("identifier", loginFixtureSuccessEmail) + values.Set(node.PasskeyLogin, string(response)) + }, testhelpers.InitFlowWithRefresh()) + snapshotx.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "0.attributes.value", + "2.attributes.nonce", + "2.attributes.src", + "5.attributes.value", + }) + nodes, err := json.Marshal(f.Ui.Nodes) + require.NoError(t, err) + assert.Equal(t, loginFixtureSuccessEmail, gjson.GetBytes(nodes, "#(attributes.name==identifier).attributes.value").String(), "%s", nodes) + + prefix := "" + if isSPA { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+login.RouteSubmitFlow, "%s", body) + prefix = "session." + } else { + assert.Contains(t, res.Request.URL.String(), fix.redirTS.URL, "%s", body) + } + + assert.True(t, gjson.Get(body, prefix+"active").Bool(), "%s", body) + + assert.EqualValues(t, expectedAAL, gjson.Get(body, prefix+"authenticator_assurance_level").String(), "%s", body) + assert.EqualValues(t, identity.CredentialsTypePasskey, gjson.Get(body, prefix+"authentication_methods.#(method==passkey).method").String(), "%s", body) + assert.Len(t, gjson.Get(body, prefix+"authentication_methods").Array(), 2, "%s", body) + assert.EqualValues(t, id.ID.String(), gjson.Get(body, prefix+"identity.id").String(), "%s", body) + } + + expectedAAL := identity.AuthenticatorAssuranceLevel1 + + for _, tc := range []struct { + creds identity.Credentials + response []byte + context []byte + descript string + }{ + { + creds: identity.Credentials{ + Config: loginPasswordlessCredentials, + Version: 1, + }, + context: loginPasswordlessContext, + response: loginPasswordlessResponse, + descript: "passwordless credentials", + }, + } { + t.Run("case=refresh "+tc.descript, func(t *testing.T) { + id := fix.createIdentityWithPasskey(t, tc.creds) + + for _, f := range []string{ + "browser", + "spa", + } { + t.Run(f, func(t *testing.T) { + run(t, ctx, id, tc.context, tc.response, f == "spa", expectedAAL) + }) + } + }) + } + }) +} + +func createIdentity(t *testing.T, ctx context.Context, reg driver.Registry, id uuid.UUID) *identity.Identity { + i := identity.NewIdentity("default") + i.SetCredentials(identity.CredentialsTypePasskey, identity.Credentials{ + Identifiers: []string{id.String()}, + Config: loginPasswordlessCredentials, + Type: identity.CredentialsTypePasskey, + Version: 1, + }) + + require.NoError(t, reg.IdentityManager().Create(ctx, i)) + return i +} + +func TestFormHydration(t *testing.T) { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + + ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".enabled", true) + ctx = configtesthelpers.WithConfigValue( + ctx, + config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".config", + map[string]interface{}{ + "rp": map[string]interface{}{ + "display_name": "foo", + "id": "localhost", + "origins": []string{"http://localhost"}, + }, + }, + ) + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/login.schema.json") + + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypePasskey) + require.NoError(t, err) + fh, ok := s.(login.FormHydrator) + require.True(t, ok) + + toSnapshot := func(t *testing.T, f *login.Flow) { + t.Helper() + // The CSRF token has a unique value that messes with the snapshot - ignore it. + f.UI.Nodes.ResetNodes("csrf_token") + f.UI.Nodes.ResetNodes("passkey_challenge") + snapshotx.SnapshotT(t, f.UI.Nodes, snapshotx.ExceptNestedKeys("nonce", "src")) + } + + newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) { + r := httptest.NewRequest("GET", "/self-service/login/browser", nil) + r = r.WithContext(ctx) + t.Helper() + f, err := login.NewFlow(conf, time.Minute, "csrf_token", r, flow.TypeBrowser) + f.UI.Nodes = make(node.Nodes, 0) + require.NoError(t, err) + return r, f + } + + t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) { + r, f := newFlow(ctx, t) + f.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodFirstFactorRefresh", func(t *testing.T) { + r, f := newFlow(ctx, t) + + id := createIdentity(t, ctx, reg, x.NewUUID()) + r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + f.Refresh = true + + require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodSecondFactorRefresh", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodSecondFactorRefresh(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) { + t.Run("case=no options", func(t *testing.T) { + t.Run("case=account enumeration mitigation disabled", func(t *testing.T) { + ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false) + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=account enumeration mitigation enabled", func(t *testing.T) { + ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true) + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + }) + + t.Run("case=WithIdentifier", func(t *testing.T) { + t.Run("case=account enumeration mitigation disabled", func(t *testing.T) { + ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false) + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=account enumeration mitigation enabled", func(t *testing.T) { + ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true) + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + }) + + t.Run("case=WithIdentityHint", func(t *testing.T) { + t.Run("case=account enumeration mitigation enabled", func(t *testing.T) { + ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true) + + id := identity.NewIdentity("test-provider") + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=account enumeration mitigation disabled", func(t *testing.T) { + ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false) + + t.Run("case=identity has passkey", func(t *testing.T) { + identifier := x.NewUUID() + id := createIdentity(t, ctx, reg, identifier) + + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id))) + toSnapshot(t, f) + }) + + t.Run("case=identity does not have a passkey", func(t *testing.T) { + id := identity.NewIdentity("default") + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + }) + }) + }) + + t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f)) + toSnapshot(t, f) + }) +} diff --git a/selfservice/strategy/passkey/passkey_registration.go b/selfservice/strategy/passkey/passkey_registration.go new file mode 100644 index 000000000000..fe3ec305e8b1 --- /dev/null +++ b/selfservice/strategy/passkey/passkey_registration.go @@ -0,0 +1,332 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey + +import ( + "context" + _ "embed" + "encoding/json" + "net/http" + "net/url" + "strings" + + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/otelx" + + "github.com/ory/kratos/x/webauthnx/js" + + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/herodot" + jsonschema "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/webauthnx" + "github.com/ory/x/randx" +) + +// Update Registration Flow with Passkey Method +// +// swagger:model updateRegistrationFlowWithPasskeyMethod +type updateRegistrationFlowWithPasskeyMethod struct { + // Register a WebAuthn Security Key + // + // It is expected that the JSON returned by the WebAuthn registration process + // is included here. + Register string `json:"passkey_register"` + + // CSRFToken is the anti-CSRF token + CSRFToken string `json:"csrf_token"` + + // The identity's traits + // + // required: true + Traits json.RawMessage `json:"traits"` + + // Method + // + // Should be set to "passkey" when trying to add, update, or remove a Passkey. + // + // required: true + Method string `json:"method"` + + // Flow is flow ID. + // + // swagger:ignore + Flow string `json:"flow"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty"` +} + +func (s *Strategy) RegisterRegistrationRoutes(r *x.RouterPublic) { + webauthnx.RegisterWebauthnRoute(r) +} + +func (s *Strategy) handleRegistrationError(_ http.ResponseWriter, r *http.Request, f *registration.Flow, p *updateRegistrationFlowWithPasskeyMethod, err error) error { + if f != nil { + if p != nil { + for _, n := range container.NewFromJSON("", node.DefaultGroup, p.Traits, "traits").Nodes { + // we only set the value and not the whole field because we want to keep types from the initial form generation + f.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue()) + } + } + + if f.Type == flow.TypeBrowser { + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + } + + return err +} + +func (s *Strategy) decode(r *http.Request) (*updateRegistrationFlowWithPasskeyMethod, error) { + var p updateRegistrationFlowWithPasskeyMethod + err := registration.DecodeBody(&p, r, s.hd, s.d.Config(), registrationSchema) + return &p, err +} + +func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, ident *identity.Identity) (err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.passkey.strategy.Register") + defer otelx.End(span, &err) + + if regFlow.Type != flow.TypeBrowser { + span.SetAttributes(attribute.String("not_responsible_reason", "flow type is not browser")) + return flow.ErrStrategyNotResponsible + } + + params, err := s.decode(r) + if err != nil { + return s.handleRegistrationError(w, r, regFlow, params, err) + } + + regFlow.TransientPayload = params.TransientPayload + + if params.Register == "" || + params.Register == "true" { // The React SDK sends "true" on empty values, so we ignore these. + span.SetAttributes(attribute.String("not_responsible_reason", "register is empty")) + return flow.ErrStrategyNotResponsible + } + + if err := flow.EnsureCSRF(s.d, r, regFlow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, params.CSRFToken); err != nil { + return s.handleRegistrationError(w, r, regFlow, params, err) + } + + params.Method = s.ID().String() + if err := flow.MethodEnabledAndAllowed(ctx, regFlow.GetFlowName(), params.Method, params.Method, s.d); err != nil { + return s.handleRegistrationError(w, r, regFlow, params, err) + } + + if len(params.Traits) == 0 { + params.Traits = json.RawMessage("{}") + } + ident.Traits = identity.Traits(params.Traits) + + webAuthnSession := gjson.GetBytes(regFlow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) + if !webAuthnSession.IsObject() { + return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object."))) + } + var webAuthnSess webauthn.SessionData + if err := json.Unmarshal([]byte(webAuthnSession.Raw), &webAuthnSess); err != nil { + return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object but got: %s", err))) + } + + if len(webAuthnSess.UserID) == 0 { + return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN session data to contain a user ID"))) + } + + webAuthnResponse, err := protocol.ParseCredentialCreationResponseBody(strings.NewReader(params.Register)) + if err != nil { + return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack( + herodot.ErrBadRequest.WithReasonf("Unable to parse WebAuthn response: %s", err))) + } + + webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) + if err != nil { + return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Unable to get webAuthn config").WithDebug(err.Error()))) + } + + credential, err := webAuthn.CreateCredential(&webauthnx.User{ + ID: webAuthnSess.UserID, + Config: webAuthn.Config, + }, webAuthnSess, webAuthnResponse) + if err != nil { + if devErr := new(protocol.Error); errors.As(err, &devErr) { + s.d.Logger().WithError(err).WithField("error_devinfo", devErr.DevInfo).Error("Failed to create WebAuthn credential") + } + return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Unable to create WebAuthn credential: %s", err))) + } + + credentialWebAuthn := identity.CredentialFromWebAuthn(credential, true) + credentialWebAuthnConfig, err := json.Marshal(identity.CredentialsWebAuthnConfig{ + Credentials: identity.CredentialsWebAuthn{*credentialWebAuthn}, + UserHandle: webAuthnSess.UserID, + }) + if err != nil { + return s.handleRegistrationError(w, r, regFlow, params, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error()))) + } + + ident.UpsertCredentialsConfig(s.ID(), credentialWebAuthnConfig, 1) + passkeyCred, _ := ident.GetCredentials(s.ID()) + passkeyCred.Identifiers = []string{string(webAuthnSess.UserID)} + ident.SetCredentials(s.ID(), *passkeyCred) + if err := s.validateCredentials(ctx, ident); err != nil { + return s.handleRegistrationError(w, r, regFlow, params, err) + } + + if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, regFlow); err != nil { + return s.handleRegistrationError(w, r, regFlow, params, err) + } + + return nil +} + +type passkeyCreateData struct { + CredentialOptions *protocol.CredentialCreation `json:"credentialOptions"` + DisplayNameFieldName string `json:"displayNameFieldName"` +} + +func (s *Strategy) PopulateRegistrationMethod(r *http.Request, regFlow *registration.Flow) error { + ctx := r.Context() + if regFlow.Type != flow.TypeBrowser { + return nil + } + + defaultSchemaURL, err := s.d.Config().DefaultIdentityTraitsSchemaURL(ctx) + if err != nil { + return err + } + nodes, err := s.populateRegistrationNodes(ctx, defaultSchemaURL) + if err != nil { + return err + } + + for _, n := range nodes { + regFlow.UI.SetNode(n) + } + + // Passkey nodes begin + createData := new(passkeyCreateData) + + fieldName, err := s.PasskeyDisplayNameFromSchema(ctx, defaultSchemaURL.String()) + if err != nil { + return err + } + createData.DisplayNameFieldName = fieldName + + webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) + if err != nil { + return errors.WithStack(err) + } + user := &webauthnx.User{ + Name: "", + ID: []byte(randx.MustString(64, randx.AlphaNum)), + Config: s.d.Config().PasskeyConfig(ctx), + } + option, sessionData, err := webAuthn.BeginRegistration(user) + if err != nil { + return errors.WithStack(err) + } + createData.CredentialOptions = option + + injectWebAuthnOptions, err := json.Marshal(createData) + if err != nil { + return errors.WithStack(err) + } + + regFlow.InternalContext, err = sjson.SetBytes( + regFlow.InternalContext, + flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), + sessionData, + ) + if err != nil { + return errors.WithStack(err) + } + + regFlow.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + + regFlow.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyCreateData, + Type: node.InputAttributeTypeHidden, + FieldValue: string(injectWebAuthnOptions), + }}) + + regFlow.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyRegister, + Type: node.InputAttributeTypeHidden, + }}) + + regFlow.UI.Nodes.Append(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{Label: text.NewInfoSelfServiceRegistrationRegisterPasskey()}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyRegisterTrigger, + Type: node.InputAttributeTypeButton, + OnClick: js.WebAuthnTriggersPasskeyRegistration.String() + "()", // defined in webauthn.js + OnClickTrigger: js.WebAuthnTriggersPasskeyRegistration, + }}) + + // Passkey nodes end + + regFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + + return nil +} + +func (s *Strategy) populateRegistrationNodes(ctx context.Context, schemaURL *url.URL) (node.Nodes, error) { + runner, err := schema.NewExtensionRunner(ctx) + if err != nil { + return nil, err + } + c := jsonschema.NewCompiler() + runner.Register(c) + + nodes, err := container.NodesFromJSONSchema(ctx, node.DefaultGroup, schemaURL.String(), "", c) + if err != nil { + return nil, err + } + + return nodes, nil +} + +func (s *Strategy) validateCredentials(ctx context.Context, i *identity.Identity) error { + if err := s.d.IdentityValidator().Validate(ctx, i); err != nil { + return err + } + + c := i.GetCredentialsOr(identity.CredentialsTypePasskey, &identity.Credentials{}) + if len(c.Identifiers) == 0 { + return schema.NewMissingIdentifierError() + } + + return nil +} diff --git a/selfservice/strategy/passkey/passkey_registration_test.go b/selfservice/strategy/passkey/passkey_registration_test.go new file mode 100644 index 000000000000..3e0338dcc357 --- /dev/null +++ b/selfservice/strategy/passkey/passkey_registration_test.go @@ -0,0 +1,474 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey_test + +import ( + _ "embed" + "net/url" + "testing" + + "github.com/ory/x/assertx" + + "github.com/ory/kratos/selfservice/flow" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal/registrationhelpers" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/x/randx" + "github.com/ory/x/sqlxx" +) + +var ( + flows = []string{"spa", "browser"} + + //go:embed fixtures/registration/success/browser/response.json + registrationFixtureSuccessResponse []byte + + //go:embed fixtures/registration/success/browser/internal_context.json + registrationFixtureSuccessBrowserInternalContext []byte + + //go:embed fixtures/registration/success/android/response.json + registrationFixtureSuccessAndroidResponse []byte + + //go:embed fixtures/registration/success/android/internal_context.json + registrationFixtureSuccessAndroidInternalContext []byte + + //go:embed fixtures/registration/failure/internal_context_missing_user_id.json + registrationFixtureFailureInternalContextMissingUserID []byte + + //go:embed fixtures/registration/failure/internal_context_wrong_user_id.json + registrationFixtureFailureInternalContextWrongUserID []byte +) + +func flowIsSPA(flow string) bool { + return flow == "spa" +} + +func TestRegistration(t *testing.T) { + t.Parallel() + + t.Run("AssertCommonErrorCases", func(t *testing.T) { + registrationhelpers.AssertCommonErrorCases(t, flows) + }) + + t.Run("AssertRegistrationRespectsValidation", func(t *testing.T) { + t.Parallel() + reg := newRegistrationRegistry(t) + registrationhelpers.AssertRegistrationRespectsValidation(t, reg, flows, func(v url.Values) { + v.Del("traits.foobar") + v.Set(node.PasskeyRegister, "{}") + v.Del("method") + }) + }) + + t.Run("AssertCSRFFailures", func(t *testing.T) { + t.Parallel() + reg := newRegistrationRegistry(t) + registrationhelpers.AssertCSRFFailures(t, reg, flows, func(v url.Values) { + v.Set(node.PasskeyRegister, "{}") + v.Del("method") + }) + }) + + t.Run("AssertSchemaDoesNotExist", func(t *testing.T) { + t.Parallel() + reg := newRegistrationRegistry(t) + registrationhelpers.AssertSchemDoesNotExist(t, reg, flows, func(v url.Values) { + v.Set(node.PasskeyRegister, "{}") + v.Del("method") + }) + }) + + t.Run("case=passkey button does not exist when passwordless is disabled", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + fix.conf.MustSet(fix.ctx, config.ViperKeyPasskeyEnabled, false) + t.Cleanup(func() { fix.conf.MustSet(fix.ctx, config.ViperKeyPasskeyEnabled, true) }) + for _, flowType := range flows { + flowType := flowType + t.Run(flowType, func(t *testing.T) { + t.Parallel() + client := testhelpers.NewClientWithCookies(t) + flo := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, fix.publicTS, flowIsSPA(flowType), false, false) + testhelpers.SnapshotTExcept(t, flo.Ui.Nodes, []string{ + "0.attributes.value", + }) + }) + } + }) + + t.Run("case=passkey button exists", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + for _, flowType := range flows { + flowType := flowType + t.Run(flowType, func(t *testing.T) { + t.Parallel() + client := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, fix.publicTS, flowIsSPA(flowType), false, false) + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "2.attributes.value", + "3.attributes.src", + "3.attributes.nonce", + "6.attributes.value", + }) + }) + } + }) + + t.Run("case=should return an error because not passing validation", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + email := testhelpers.RandomEmail() + + var values = func(v url.Values) { + v.Set("traits.username", email) + v.Del("traits.foobar") + v.Set(node.PasskeyRegister, "{}") + v.Del("method") + } + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + actual := registrationhelpers.ExpectValidationError(t, fix.publicTS, fix.conf, f, values) + + assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "ui.action").String(), fix.publicTS.URL+registration.RouteSubmitFlow, "%s", actual) + registrationhelpers.CheckFormContent(t, []byte(actual), "csrf_token", "traits.username", "traits.foobar") + assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0").String(), `Property foobar is missing`, "%s", actual) + assert.Equal(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", actual) + }) + } + }) + + t.Run("case=should reject invalid transient payload", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + email := testhelpers.RandomEmail() + + var values = func(v url.Values) { + v.Set("traits.username", email) + v.Set("traits.foobar", "bar") + v.Set("transient_payload", "42") + v.Set(node.PasskeyRegister, "{}") + v.Del("method") + } + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + actual := registrationhelpers.ExpectValidationError(t, fix.publicTS, fix.conf, f, values) + + assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "ui.action").String(), fix.publicTS.URL+registration.RouteSubmitFlow, "%s", actual) + registrationhelpers.CheckFormContent(t, []byte(actual), "csrf_token", "traits.username", "traits.foobar") + assert.Equal(t, "bar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).attributes.value").String(), "%s", actual) + assert.Equal(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", actual) + assert.Equal(t, int64(4000026), gjson.Get(actual, "ui.nodes.#(attributes.name==transient_payload).messages.0.id").Int(), "%s", actual) + }) + } + }) + + t.Run("case=should return an error because passkey response is invalid", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + email := testhelpers.RandomEmail() + + var values = func(v url.Values) { + v.Set("traits.username", email) + v.Set("traits.foobar", "bazbar") + v.Set(node.PasskeyRegister, "invalid") + v.Set("method", "passkey") + } + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + actual, _, _ := fix.submitPasskeyBrowserRegistration(t, f, testhelpers.NewClientWithCookies(t), values) + assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "ui.action").String(), fix.publicTS.URL+registration.RouteSubmitFlow, "%s", actual) + registrationhelpers.CheckFormContent(t, []byte(actual), node.PasskeyRegister, "csrf_token", "traits.username", "traits.foobar") + assert.Equal(t, "bazbar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).attributes.value").String(), "%s", actual) + assert.Equal(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "ui.messages.0").String(), `Unable to parse WebAuthn response: Parse error for Registration`, "%s", actual) + }) + } + }) + + t.Run("case=should return an error because internal context is invalid", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + email := testhelpers.RandomEmail() + + for _, tc := range []struct { + name string + internalContext string + }{{ + name: "invalid json", + internalContext: "invalid", + }, { + name: "missing user ID", + internalContext: string(registrationFixtureFailureInternalContextMissingUserID), + }, { + name: "wrong user ID", + internalContext: string(registrationFixtureFailureInternalContextWrongUserID), + }} { + tc := tc + t.Run("context="+tc.name, func(t *testing.T) { + var values = func(v url.Values) { + v.Set("traits.username", email) + v.Set("traits.foobar", "bazbar") + v.Set(node.PasskeyRegister, string(registrationFixtureSuccessResponse)) + v.Del("method") + } + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + actual, _, _ := fix.submitPasskeyBrowserRegistration(t, f, testhelpers.NewClientWithCookies(t), values, + withInternalContext(sqlxx.JSONRawMessage(tc.internalContext))) + if flowIsSPA(f) { + assert.Equal(t, "Internal Server Error", gjson.Get(actual, "error.status").String(), "%s", actual) + } else { + assert.Equal(t, "Internal Server Error", gjson.Get(actual, "status").String(), "%s", actual) + } + }) + } + }) + } + }) + + t.Run("case=should fail to create identity if schema is missing the identifier", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/noid.schema.json") + email := testhelpers.RandomEmail() + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + isSPA := f == "spa" + regFlow := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, fix.publicTS, isSPA, false, false) + + // fill out traits and click on "sign up with passkey" + urlValues := testhelpers.SDKFormFieldsToURLValues(regFlow.Ui.Nodes) + urlValues.Set("traits.email", email) + urlValues.Set("method", "passkey") + actual, _ := testhelpers.RegistrationMakeRequest(t, false, isSPA, regFlow, client, urlValues.Encode()) + + assert.Contains(t, gjson.Get(actual, "ui.action").String(), fix.publicTS.URL+registration.RouteSubmitFlow, "%s", actual) + registrationhelpers.CheckFormContent(t, []byte(actual), "csrf_token", "traits.email") + assert.Equal(t, text.NewErrorValidationRegistrationNoStrategyFound().Text, gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) + }) + } + }) + + getPrefix := func(f string) (prefix string) { + if f == "spa" { + prefix = "session." + } + return + } + + t.Run("successful registration", func(t *testing.T) { + t.Parallel() + fix := newRegistrationFixture(t) + t.Cleanup(fix.disableSessionAfterRegistration) + + var values = func(email string) func(v url.Values) { + return func(v url.Values) { + v.Set("traits.username", email) + v.Set("traits.foobar", "bazbar") + v.Set(node.PasskeyRegister, string(registrationFixtureSuccessResponse)) + v.Del("method") + } + } + + t.Run("case=should create the identity but not a session", func(t *testing.T) { + fix.useRedirNoSessionTS() + t.Cleanup(fix.useRedirTS) + fix.disableSessionAfterRegistration() + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := f + "-" + testhelpers.RandomEmail() + userID := f + "-user-" + randx.MustString(8, randx.AlphaNum) + actual := fix.makeSuccessfulRegistration(t, f, fix.redirNoSessionTS.URL+"/registration-return-ts", values(email), withUserID(userID)) + + if f == "spa" { + assert.Equal(t, email, gjson.Get(actual, "identity.traits.username").String(), "%s", actual) + assert.False(t, gjson.Get(actual, "session").Exists(), "because the registration yielded no session, the user is not expected to be signed in: %s", actual) + } else { + assert.Equal(t, "null\n", actual, "because the registration yielded no session, the user is not expected to be signed in: %s", actual) + } + + i, _, err := fix.reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(fix.ctx, identity.CredentialsTypePasskey, userID) + require.NoError(t, err) + assert.Equal(t, "aal1", i.InternalAvailableAAL.String) + assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + }) + } + }) + + t.Run("case=should accept valid transient payload", func(t *testing.T) { + fix.useRedirNoSessionTS() + t.Cleanup(fix.useRedirTS) + fix.disableSessionAfterRegistration() + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := testhelpers.RandomEmail() + userID := f + "-user-" + randx.MustString(8, randx.AlphaNum) + actual := fix.makeSuccessfulRegistration(t, f, fix.redirNoSessionTS.URL+"/registration-return-ts", func(v url.Values) { + values(email)(v) + v.Set("transient_payload.stuff", "42") + }, withUserID(userID)) + + if f == "spa" { + assert.Equal(t, email, gjson.Get(actual, "identity.traits.username").String(), "%s", actual) + assert.False(t, gjson.Get(actual, "session").Exists(), "because the registration yielded no session, the user is not expected to be signed in: %s", actual) + } else { + assert.Equal(t, "null\n", actual, "because the registration yielded no session, the user is not expected to be signed in: %s", actual) + } + + i, _, err := fix.reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(fix.ctx, identity.CredentialsTypePasskey, userID) + require.NoError(t, err) + assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + + if f == "spa" { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), fix.redirNoSessionTS.URL+"/registration-return-ts", "%s", actual) + } else { + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) + } + }) + } + }) + + t.Run("case=should create the identity and a session and use the correct schema", func(t *testing.T) { + fix.enableSessionAfterRegistration() + fix.conf.MustSet(fix.ctx, config.ViperKeyDefaultIdentitySchemaID, "advanced-user") + fix.conf.MustSet(fix.ctx, config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "does-not-exist", URL: "file://./stub/profile.schema.json"}, + {ID: "advanced-user", URL: "file://./stub/registration.schema.json"}, + }) + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := testhelpers.RandomEmail() + userID := f + "-user-" + randx.MustString(8, randx.AlphaNum) + actual := fix.makeSuccessfulRegistration(t, f, fix.redirTS.URL+"/registration-return-ts", values(email), withUserID(userID)) + + prefix := getPrefix(f) + + assert.Equal(t, email, gjson.Get(actual, prefix+"identity.traits.username").String(), "%s", actual) + assert.True(t, gjson.Get(actual, prefix+"active").Bool(), "%s", actual) + + i, _, err := fix.reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(fix.ctx, identity.CredentialsTypePasskey, userID) + require.NoError(t, err) + assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + }) + } + }) + + t.Run("case=not able to create the same account twice", func(t *testing.T) { + fix.enableSessionAfterRegistration() + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/registration.schema.json") + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := testhelpers.RandomEmail() + userID := f + "-user-" + randx.MustString(8, randx.AlphaNum) + actual := fix.makeSuccessfulRegistration(t, f, fix.redirTS.URL+"/registration-return-ts", values(email), withUserID(userID)) + assert.True(t, gjson.Get(actual, getPrefix(f)+"active").Bool(), "%s", actual) + + actual, _, _ = fix.makeRegistration(t, f, values(email)) + assert.Contains(t, gjson.Get(actual, "ui.action").String(), fix.publicTS.URL+registration.RouteSubmitFlow, "%s", actual) + registrationhelpers.CheckFormContent(t, []byte(actual), "csrf_token", "traits.username") + assert.Equal(t, + "You tried signing in with "+email+" which is already in use by another account. You can sign in using your passkey.", + gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) + }) + } + }) + + t.Run("case=reset previous form errors", func(t *testing.T) { + fix.enableSessionAfterRegistration() + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/registration.schema.json") + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := testhelpers.RandomEmail() + actual, _, _ := fix.makeRegistration(t, f, func(v url.Values) { + v.Del("traits.username") + v.Set("traits.foobar", "bazbar") + v.Set(node.PasskeyRegister, string(registrationFixtureSuccessResponse)) + }) + registrationhelpers.CheckFormContent(t, []byte(actual), "csrf_token", "traits.username", "traits.foobar") + assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).messages.0").String(), `Property username is missing`, "%s", actual) + + actual, _, _ = fix.makeRegistration(t, f, func(v url.Values) { + v.Set("traits.username", email) + v.Del("traits.foobar") + v.Set(node.PasskeyRegister, string(registrationFixtureSuccessResponse)) + v.Del("method") + }) + registrationhelpers.CheckFormContent(t, []byte(actual), "csrf_token", "traits.username", "traits.foobar") + assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0").String(), `Property foobar is missing`, "%s", actual) + assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).messages").Array()) + assert.Empty(t, gjson.Get(actual, "ui.nodes.messages").Array()) + }) + } + }) + + t.Run("case=should create the identity when using android", func(t *testing.T) { + fix.useRedirNoSessionTS() + t.Cleanup(fix.useRedirTS) + fix.disableSessionAfterRegistration() + + prevRPID := fix.conf.GetProvider(fix.ctx).String(config.ViperKeyPasskeyRPID) + prevOrigins := fix.conf.GetProvider(fix.ctx).String(config.ViperKeyPasskeyRPOrigins) + + fix.conf.MustSet(fix.ctx, config.ViperKeyPasskeyRPID, "www.troweprice.com") + fix.conf.MustSet(fix.ctx, config.ViperKeyPasskeyRPOrigins, []string{"android:apk-key-hash:S2RfNYgJmQiKgd6-sdbjW7phcL_OTP4vGE8L51Q2GB0"}) + t.Cleanup(func() { + fix.conf.MustSet(fix.ctx, config.ViperKeyPasskeyRPID, prevRPID) + fix.conf.MustSet(fix.ctx, config.ViperKeyPasskeyRPOrigins, prevOrigins) + }) + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + email := f + "-" + testhelpers.RandomEmail() + userID := f + "-user-" + randx.MustString(8, randx.AlphaNum) + + expectReturnTo := fix.redirNoSessionTS.URL + "/registration-return-ts" + actual, res, _ := fix.submitPasskeyAndroidRegistration(t, f, testhelpers.NewClientWithCookies(t), func(v url.Values) { + values(email)(v) + v.Set(node.PasskeyRegister, string(registrationFixtureSuccessAndroidResponse)) + }, withUserID(userID)) + + if f == "spa" { + expectReturnTo = fix.publicTS.URL + assert.Equal(t, email, gjson.Get(actual, "identity.traits.username").String(), "%s", actual) + assert.False(t, gjson.Get(actual, "session").Exists(), "because the registration yielded no session, the user is not expected to be signed in: %s", actual) + } else { + assert.Equal(t, "null\n", actual, "because the registration yielded no session, the user is not expected to be signed in: %s", actual) + } + + assert.Contains(t, res.Request.URL.String(), expectReturnTo, "%+v\n\t%s", res.Request, assertx.PrettifyJSONPayload(t, actual)) + + i, _, err := fix.reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(fix.ctx, identity.CredentialsTypePasskey, userID) + require.NoError(t, err) + assert.Equal(t, "aal1", i.InternalAvailableAAL.String) + assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + }) + } + }) + }) +} diff --git a/selfservice/strategy/passkey/passkey_schema_extension.go b/selfservice/strategy/passkey/passkey_schema_extension.go new file mode 100644 index 000000000000..a3d7b32c6e6d --- /dev/null +++ b/selfservice/strategy/passkey/passkey_schema_extension.go @@ -0,0 +1,111 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey + +import ( + "context" + "errors" + "fmt" + "strings" + "sync" + + "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" + "github.com/ory/x/stringsx" +) + +type SchemaExtension struct { + WebauthnIdentifier string + PasskeyDisplayName string + sync.Mutex +} + +func (e *SchemaExtension) Run(_ jsonschema.ValidationContext, s schema.ExtensionConfig, value any) error { + e.Lock() + defer e.Unlock() + + if s.Credentials.WebAuthn.Identifier { + e.WebauthnIdentifier = strings.ToLower(fmt.Sprintf("%s", value)) + } + + if s.Credentials.Passkey.DisplayName { + e.PasskeyDisplayName = fmt.Sprintf("%s", value) + } + + return nil +} + +func (e *SchemaExtension) Finish() error { return nil } + +// PasskeyDisplayNameFromIdentity returns the passkey display name from the +// identity. It is usually the email address and used to name the passkey in the +// browser. +func (s *Strategy) PasskeyDisplayNameFromIdentity(ctx context.Context, id *identity.Identity) string { + e := new(SchemaExtension) + // We can ignore teh error here because proper validation happens once the identity is persisted. + _ = s.d.IdentityValidator().ValidateWithRunner(ctx, id, e) + + return stringsx.Coalesce(e.PasskeyDisplayName, e.WebauthnIdentifier) +} + +func (s *Strategy) PasskeyDisplayNameFromTraits(ctx context.Context, traits identity.Traits) string { + id := identity.NewIdentity("") + id.Traits = traits + + return s.PasskeyDisplayNameFromIdentity(ctx, id) +} + +func (s *Strategy) PasskeyDisplayNameFromSchema(ctx context.Context, schemaURL string) (string, error) { + ext := &passkeyDisplayNameExtension{} + + runner, err := schema.NewExtensionRunner(ctx, schema.WithCompileRunners(ext)) + if err != nil { + return "", err + } + c := jsonschema.NewCompiler() + c.ExtractAnnotations = true + runner.Register(c) + + schem, err := c.Compile(ctx, schemaURL) + if err != nil { + return "", err + } + + for key, value := range schem.Properties["traits"].Properties { + if value.Title == ext.getLabel() { + return "traits." + key, nil + } + } + + return "", errors.New("no identifier found") +} + +type passkeyDisplayNameExtension struct { + identifierLabelCandidates []string +} + +func (i *passkeyDisplayNameExtension) Run(_ jsonschema.CompilerContext, config schema.ExtensionConfig, rawSchema map[string]interface{}) error { + if config.Credentials.WebAuthn.Identifier || + config.Credentials.Passkey.DisplayName { + if title, ok := rawSchema["title"]; ok { + // The jsonschema compiler validates the title to be a string, so this should always work. + switch t := title.(type) { + case string: + if t != "" { + i.identifierLabelCandidates = append(i.identifierLabelCandidates, t) + } + } + } + } + return nil +} + +func (i *passkeyDisplayNameExtension) getLabel() string { + if len(i.identifierLabelCandidates) != 1 { + // sane default is set elsewhere + return "" + } + return i.identifierLabelCandidates[0] +} diff --git a/selfservice/strategy/passkey/passkey_settings.go b/selfservice/strategy/passkey/passkey_settings.go new file mode 100644 index 000000000000..6d5e73b808e8 --- /dev/null +++ b/selfservice/strategy/passkey/passkey_settings.go @@ -0,0 +1,437 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/otelx" + + "github.com/ory/kratos/x/webauthnx/js" + + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/herodot" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/kratos/x/webauthnx" + "github.com/ory/x/decoderx" + "github.com/ory/x/randx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/sqlxx" +) + +func (s *Strategy) RegisterSettingsRoutes(_ *x.RouterPublic) {} + +func (s *Strategy) SettingsStrategyID() string { return s.ID().String() } + +const ( + InternalContextKeySessionData = "session_data" +) + +func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity, f *settings.Flow) error { + if f.Type != flow.TypeBrowser { + return nil + } + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + + confidentialIdentity, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), id.ID) + if err != nil { + return err + } + + count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(r.Context(), confidentialIdentity) + if err != nil { + return err + } + + if webAuthns, err := s.identityListWebAuthn(confidentialIdentity); errors.Is(err, sqlcon.ErrNoRows) { + // Do nothing + } else if err != nil { + return err + } else { + for k := range webAuthns.Credentials { + // We only show the option to remove a credential, if it is not the last one when passwordless, + // or, if it is for MFA we show it always. + cred := &webAuthns.Credentials[k] + + f.UI.Nodes.Append(webauthnx.NewPasskeyUnlink(cred, func(a *node.InputAttributes) { + // Do not remove this node if it is the last credential the identity can sign in with. + a.Disabled = count < 2 + })) + } + } + + web, err := webauthn.New(s.d.Config().PasskeyConfig(r.Context())) + if err != nil { + return errors.WithStack(err) + } + + identifier := s.PasskeyDisplayNameFromIdentity(r.Context(), id) + if identifier == "" { + f.UI.Messages.Add(text.NewErrorValidationIdentifierMissing()) + return nil + } + + user := &webauthnx.User{ + Name: identifier, + ID: []byte(randx.MustString(64, randx.AlphaNum)), + Config: s.d.Config().PasskeyConfig(r.Context()), + } + option, sessionData, err := web.BeginRegistration(user) + if err != nil { + return errors.WithStack(err) + } + + f.InternalContext, err = sjson.SetBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData), sessionData) + if err != nil { + return errors.WithStack(err) + } + + injectWebAuthnOptions, err := json.Marshal(option) + if err != nil { + return errors.WithStack(err) + } + + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(r.Context()))) + + f.UI.Nodes.Upsert(node.NewInputField( + node.PasskeyRegisterTrigger, + "", + node.PasskeyGroup, + node.InputAttributeTypeButton, + node.WithInputAttributes(func(a *node.InputAttributes) { + //nolint:staticcheck + a.OnClick = js.WebAuthnTriggersPasskeySettingsRegistration.String() + "()" + a.OnClickTrigger = js.WebAuthnTriggersPasskeySettingsRegistration + }), + ).WithMetaLabel(text.NewInfoSelfServiceSettingsRegisterPasskey())) + + f.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeySettingsRegister, + Type: node.InputAttributeTypeHidden, + }, + }) + + f.UI.Nodes.Upsert(&node.Node{ + Type: node.Input, + Group: node.PasskeyGroup, + Meta: &node.Meta{}, + Attributes: &node.InputAttributes{ + Name: node.PasskeyCreateData, + Type: node.InputAttributeTypeHidden, + FieldValue: string(injectWebAuthnOptions), + }, + }) + + return nil +} + +func (s *Strategy) identityListWebAuthn(id *identity.Identity) (*identity.CredentialsWebAuthnConfig, error) { + cred, ok := id.GetCredentials(s.ID()) + if !ok { + return nil, errors.WithStack(sqlcon.ErrNoRows) + } + + var cc identity.CredentialsWebAuthnConfig + if err := json.Unmarshal(cred.Config, &cc); err != nil { + return nil, errors.WithStack(err) + } + + return &cc, nil +} + +func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (_ *settings.UpdateContext, err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.passkey.strategy.Settings") + defer otelx.End(span, &err) + + if f.Type != flow.TypeBrowser { + span.SetAttributes(attribute.String("not_responsible_reason", "not a browser flow")) + return nil, errors.WithStack(flow.ErrStrategyNotResponsible) + } + var p updateSettingsFlowWithPasskeyMethod + ctxUpdate, err := settings.PrepareUpdate(s.d, w, r, f, ss, settings.ContinuityKey(s.SettingsStrategyID()), &p) + if errors.Is(err, settings.ErrContinuePreviousAction) { + return ctxUpdate, s.continueSettingsFlow(ctx, w, r, ctxUpdate, p) + } else if err != nil { + return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + if err := s.decodeSettingsFlow(r, &p); err != nil { + return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + if len(p.Register)+len(p.Remove) > 0 { + // This method has only two submit buttons + p.Method = s.SettingsStrategyID() + if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + return nil, s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + } else { + span.SetAttributes(attribute.String("not_responsible_reason", "neither register nor remove provided")) + return nil, errors.WithStack(flow.ErrStrategyNotResponsible) + } + + // This does not come from the payload! + p.Flow = ctxUpdate.Flow.ID.String() + if err := s.continueSettingsFlow(ctx, w, r, ctxUpdate, p); err != nil { + return ctxUpdate, s.handleSettingsError(ctx, w, r, ctxUpdate, p, err) + } + + return ctxUpdate, nil +} + +// Update Settings Flow with Passkey Method +// +// swagger:model updateSettingsFlowWithPasskeyMethod +type updateSettingsFlowWithPasskeyMethod struct { + // Register a WebAuthn Security Key + // + // It is expected that the JSON returned by the WebAuthn registration process + // is included here. + Register string `json:"passkey_settings_register"` + + // Remove a WebAuthn Security Key + // + // This must contain the ID of the WebAuthN connection. + Remove string `json:"passkey_remove"` + + // CSRFToken is the anti-CSRF token + CSRFToken string `json:"csrf_token"` + + // Method + // + // Should be set to "passkey" when trying to add, update, or remove a webAuthn pairing. + // + // required: true + Method string `json:"method"` + + // Flow is flow ID. + // + // swagger:ignore + Flow string `json:"flow"` +} + +func (p *updateSettingsFlowWithPasskeyMethod) GetFlowID() uuid.UUID { + return x.ParseUUID(p.Flow) +} + +func (p *updateSettingsFlowWithPasskeyMethod) SetFlowID(rid uuid.UUID) { + p.Flow = rid.String() +} + +func (s *Strategy) continueSettingsFlow( + ctx context.Context, + w http.ResponseWriter, r *http.Request, + ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithPasskeyMethod, +) error { + if len(p.Register+p.Remove) > 0 { + if err := flow.MethodEnabledAndAllowed(ctx, flow.SettingsFlow, s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { + return err + } + + if err := flow.EnsureCSRF(s.d, r, ctxUpdate.Flow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + return err + } + + if ctxUpdate.Session.AuthenticatedAt.Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(ctx)).Before(time.Now()) { + return errors.WithStack(settings.NewFlowNeedsReAuth()) + } + } else { + return errors.New("ended up in unexpected state") + } + + switch { + case len(p.Remove) > 0: + return s.continueSettingsFlowRemove(ctx, w, r, ctxUpdate, p) + case len(p.Register) > 0: + return s.continueSettingsFlowAdd(ctx, ctxUpdate, p) + default: + return errors.New("ended up in unexpected state") + } +} + +func (s *Strategy) continueSettingsFlowRemove(ctx context.Context, w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithPasskeyMethod) error { + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ctxUpdate.Session.IdentityID) + if err != nil { + return err + } + + cred, ok := i.GetCredentials(s.ID()) + if !ok { + return errors.WithStack(herodot.ErrBadRequest.WithReasonf("You tried to remove a WebAuthn but you have no WebAuthn set up.")) + } + + var cc identity.CredentialsWebAuthnConfig + if err := json.Unmarshal(cred.Config, &cc); err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error())) + } + + updated := make([]identity.CredentialWebAuthn, 0) + for k, cred := range cc.Credentials { + if fmt.Sprintf("%x", cred.ID) != p.Remove { + updated = append(updated, cc.Credentials[k]) + } + } + + if len(updated) == len(cc.Credentials) { + return errors.WithStack(herodot.ErrBadRequest.WithReasonf("You tried to remove a passkey which does not exist.")) + } + + count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(ctx, i) + if err != nil { + return err + } + + if count < 2 { + return s.handleSettingsError(ctx, w, r, ctxUpdate, p, errors.WithStack(webauthnx.ErrNotEnoughCredentials)) + } + + if len(updated) == 0 { + i.DeleteCredentialsType(identity.CredentialsTypePasskey) + ctxUpdate.UpdateIdentity(i) + return nil + } + + cc.Credentials = updated + cred.Config, err = json.Marshal(cc) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error())) + } + + i.SetCredentials(s.ID(), *cred) + ctxUpdate.UpdateIdentity(i) + return nil +} + +func (s *Strategy) continueSettingsFlowAdd(ctx context.Context, ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithPasskeyMethod) error { + webAuthnSession := gjson.GetBytes(ctxUpdate.Flow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) + if !webAuthnSession.IsObject() { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object.")) + } + + var webAuthnSess webauthn.SessionData + if err := json.Unmarshal([]byte(gjson.GetBytes(ctxUpdate.Flow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)).Raw), &webAuthnSess); err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object but got: %s", err)) + } + + webAuthnResponse, err := protocol.ParseCredentialCreationResponseBody(strings.NewReader(p.Register)) + if err != nil { + return errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to parse WebAuthn response: %s", err)) + } + + web, err := webauthn.New(s.d.Config().PasskeyConfig(ctx)) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error())) + } + + credential, err := web.CreateCredential(&webauthnx.User{ + ID: webAuthnSess.UserID, + Config: web.Config, + }, webAuthnSess, webAuthnResponse) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to create WebAuthn credential: %s", err)) + } + + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ctxUpdate.Session.IdentityID) + if err != nil { + return err + } + + cred := i.GetCredentialsOr(s.ID(), &identity.Credentials{Config: sqlxx.JSONRawMessage("{}")}) + + var cc identity.CredentialsWebAuthnConfig + if err := json.Unmarshal(cred.Config, &cc); err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error())) + } + + credentialWebAuthn := identity.CredentialFromWebAuthn(credential, true) + cc.UserHandle = webAuthnSess.UserID + cc.Credentials = append(cc.Credentials, *credentialWebAuthn) + credentialsConfig, err := json.Marshal(cc) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error())) + } + + i.UpsertCredentialsConfig(s.ID(), credentialsConfig, 1, identity.WithAdditionalIdentifier(string(webAuthnSess.UserID))) + if err := s.validateCredentials(ctx, i); err != nil { + return err + } + + // Remove the WebAuthn URL from the internal context now that it is set! + ctxUpdate.Flow.InternalContext, err = sjson.DeleteBytes(ctxUpdate.Flow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) + if err != nil { + return err + } + + if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(ctx, ctxUpdate.Flow); err != nil { + return err + } + + aal := identity.AuthenticatorAssuranceLevel1 + + // Since we added the method, it also means that we have authenticated it + if err := s.d.SessionManager().SessionAddAuthenticationMethods(ctx, ctxUpdate.Session.ID, session.AuthenticationMethod{ + Method: s.ID(), + AAL: aal, + }); err != nil { + return err + } + + ctxUpdate.UpdateIdentity(i) + return nil +} + +func (s *Strategy) decodeSettingsFlow(r *http.Request, dest interface{}) error { + compiler, err := decoderx.HTTPRawJSONSchemaCompiler(settingsSchema) + if err != nil { + return errors.WithStack(err) + } + + return decoderx.NewHTTP().Decode(r, dest, compiler, + decoderx.HTTPKeepRequestBody(true), + decoderx.HTTPDecoderAllowedMethods("POST", "GET"), + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.HTTPDecoderJSONFollowsFormFormat(), + ) +} + +func (s *Strategy) handleSettingsError(ctx context.Context, w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithPasskeyMethod, err error) error { + // Do not pause flow if the flow type is an API flow as we can't save cookies in those flows. + if e := new(settings.FlowNeedsReAuth); errors.As(err, &e) && ctxUpdate.Flow != nil && ctxUpdate.Flow.Type == flow.TypeBrowser { + if err := s.d.ContinuityManager().Pause(ctx, w, r, settings.ContinuityKey(s.SettingsStrategyID()), settings.ContinuityOptions(p, ctxUpdate.GetSessionIdentity())...); err != nil { + return err + } + } + + if ctxUpdate.Flow != nil { + ctxUpdate.Flow.UI.ResetMessages() + ctxUpdate.Flow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + + return err +} diff --git a/selfservice/strategy/passkey/passkey_settings_test.go b/selfservice/strategy/passkey/passkey_settings_test.go new file mode 100644 index 000000000000..781af829be3e --- /dev/null +++ b/selfservice/strategy/passkey/passkey_settings_test.go @@ -0,0 +1,448 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey_test + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + "net/http" + "net/url" + "testing" + + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/strategy/passkey" + + "github.com/ory/x/snapshotx" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + + "github.com/stretchr/testify/require" + + "github.com/ory/x/assertx" + "github.com/ory/x/sqlxx" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/x" +) + +//go:embed fixtures/settings/success/identity.json +var settingsFixtureSuccessIdentity []byte + +//go:embed fixtures/settings/success/response.json +var settingsFixtureSuccessResponse []byte + +//go:embed fixtures/settings/success/internal_context.json +var settingsFixtureSuccessInternalContext []byte + +var ctx = context.Background() + +func TestCompleteSettings(t *testing.T) { + fix := newSettingsFixture(t) + + t.Run("case=invalid passkey config", func(t *testing.T) { + fix := newSettingsFixture(t) + fix.conf.MustSet(ctx, config.ViperKeyPasskeyRPID, "") + id := fix.createIdentity(t) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, id) + + req, err := http.NewRequest("GET", fix.publicTS.URL+settings.RouteInitBrowserFlow, nil) + require.NoError(t, err) + req.Header.Set("Accept", "application/json") + res, err := apiClient.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + }) + + t.Run("case=a device is shown which can be unlinked", func(t *testing.T) { + id := fix.createIdentity(t) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, fix.publicTS) + + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "4.attributes.value", // passkey_settings_register + "5.attributes.value", // CSRF + "6.attributes.nonce", // script + "6.attributes.src", // script + }) + }) + + t.Run("case=one activation element is shown", func(t *testing.T) { + id := fix.createIdentityWithoutPasskey(t) + require.NoError(t, fix.reg.PrivilegedIdentityPool().UpdateIdentity(fix.ctx, id)) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, fix.publicTS) + + testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ + "2.attributes.value", // passkey_create_data + "3.attributes.value", // CSRF + "4.attributes.nonce", // script + "4.attributes.src", // script + }) + }) + + t.Run("case=passkeys only work for browsers", func(t *testing.T) { + id := fix.createIdentityWithoutPasskey(t) + require.NoError(t, fix.reg.PrivilegedIdentityPool().UpdateIdentity(fix.ctx, id)) + + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, fix.reg, id) + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, fix.publicTS) + for _, n := range f.Ui.Nodes { + assert.NotEqual(t, n.Group, "passkey", "unexpected group: %s", n.Group) + } + }) + + doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) { + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, fix.reg, id) + f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, fix.publicTS) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + v(values) + payload := testhelpers.EncodeFormAsJSON(t, true, values) + return testhelpers.SettingsMakeRequest(t, true, false, f, apiClient, payload) + } + + doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) { + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, fix.publicTS) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + v(values) + return testhelpers.SettingsMakeRequest(t, false, spa, f, browserClient, testhelpers.EncodeFormAsJSON(t, spa, values)) + } + + t.Run("case=fails with api submit because only browsers are supported", func(t *testing.T) { + id := fix.createIdentityWithoutPasskey(t) + body, res := doAPIFlow(t, func(v url.Values) { + v.Set(node.PasskeySettingsRegister, "{}") + v.Set("method", "passkey") + }, id) + + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + assert.Equal(t, text.NewErrorValidationSettingsNoStrategyFound().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + }) + + t.Run("case=fails with browser submit because csrf token is missing", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + id := fix.createIdentityWithoutPasskey(t) + body, res := doBrowserFlow(t, spa, func(v url.Values) { + v.Del("csrf_token") + v.Set(node.PasskeySettingsRegister, "{}") + v.Set("method", "passkey") + }, id) + if spa { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + assert.Equal(t, x.ErrInvalidCSRFToken.Reason(), gjson.Get(body, "error.reason").String(), body) + } else { + assert.Contains(t, res.Request.URL.String(), fix.errTS.URL) + assert.Equal(t, x.ErrInvalidCSRFToken.Reason(), gjson.Get(body, "reason").String(), body) + } + } + + t.Run("type=browser", func(t *testing.T) { + run(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true) + }) + }) + + t.Run("case=fails with browser submit register payload is invalid", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + id := fix.createIdentityWithoutPasskey(t) + body, res := doBrowserFlow(t, spa, func(v url.Values) { + v.Set(node.PasskeySettingsRegister, "{}") + v.Set("method", "passkey") + }, id) + if spa { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + } else { + assert.Contains(t, res.Request.URL.String(), fix.uiTS.URL) + } + assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "Parse error for Registration", "%s", body) + } + + t.Run("type=browser", func(t *testing.T) { + run(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true) + }) + }) + + t.Run("case=requires privileged session for register", func(t *testing.T) { + fix.conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1ns") + t.Cleanup(func() { + fix.conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "5m") + }) + + run := func(t *testing.T, spa bool) { + id := fix.createIdentityWithoutPasskey(t) + + body, res := doBrowserFlow(t, spa, func(v url.Values) { + v.Set(node.PasskeySettingsRegister, "{}") + v.Set("method", "passkey") + }, id) + + if spa { + assert.NotEmpty(t, gjson.Get(body, "redirect_browser_to").String()) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assertx.EqualAsJSONExcept(t, settings.NewFlowNeedsReAuth(), json.RawMessage(body), []string{"redirect_browser_to"}) + } else { + assert.Contains(t, res.Request.URL.String(), fix.loginTS.URL+"/login-ts") + assertx.EqualAsJSON(t, text.NewInfoLoginReAuth(), json.RawMessage(gjson.Get(body, "ui.messages.0").Raw)) + } + } + + t.Run("type=browser", func(t *testing.T) { + run(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true) + }) + }) + + t.Run("case=add a passkey", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + // We load our identity which we will use to replay the webauth session + var id identity.Identity + require.NoError(t, json.Unmarshal(settingsFixtureSuccessIdentity, &id)) + id.NID = x.NewUUID() + _ = fix.reg.PrivilegedIdentityPool().DeleteIdentity(fix.ctx, id.ID) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, &id) + f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, fix.publicTS) + + // We inject the session to replay + interim, err := fix.reg.SettingsFlowPersister().GetSettingsFlow(fix.ctx, uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = settingsFixtureSuccessInternalContext + require.NoError(t, fix.reg.SettingsFlowPersister().UpdateSettingsFlow(fix.ctx, interim)) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + + // We use the response replay + values.Set("method", "passkey") + values.Set(node.PasskeySettingsRegister, string(settingsFixtureSuccessResponse)) + body, res := testhelpers.SettingsMakeRequest(t, false, spa, f, browserClient, testhelpers.EncodeFormAsJSON(t, spa, values)) + require.Equal(t, http.StatusOK, res.StatusCode, "%s", body) + + if spa { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + } else { + assert.Contains(t, res.Request.URL.String(), fix.uiTS.URL) + } + assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body) + + actual, err := fix.reg.Persister().GetIdentityConfidential(fix.ctx, id.ID) + require.NoError(t, err) + cred, ok := actual.GetCredentials(identity.CredentialsTypePasskey) + require.True(t, ok) + assert.Len(t, gjson.GetBytes(cred.Config, "credentials").Array(), 1) + + actualFlow, err := fix.reg.SettingsFlowPersister().GetSettingsFlow(fix.ctx, uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + // new session data has been generated + assert.NotEqual(t, settingsFixtureSuccessInternalContext, + gjson.GetBytes(actualFlow.InternalContext, + flow.PrefixInternalContextKey(identity.CredentialsTypePasskey, passkey.InternalContextKeySessionData))) + + testhelpers.EnsureAAL(t, browserClient, fix.publicTS, "aal1", string(identity.CredentialsTypePasskey)) + + if spa { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), fix.uiTS.URL, "%s", body) + } else { + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) + } + } + + t.Run("type=browser", func(t *testing.T) { + run(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true) + }) + }) + + t.Run("case=fails to remove passkey if it is the last credential available", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + id := fix.createIdentity(t) + id.DeleteCredentialsType(identity.CredentialsTypePassword) + conf := sqlxx.JSONRawMessage(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`) + id.UpsertCredentialsConfig(identity.CredentialsTypePasskey, conf, 0) + require.NoError(t, fix.reg.IdentityManager().Update(ctx, id, identity.ManagerAllowWriteProtectedTraits)) + + body, res := doBrowserFlow(t, spa, func(v url.Values) { + // The remove key should be empty + snapshotx.SnapshotT(t, v, snapshotx.ExceptPaths("csrf_token", "passkey_create_data")) + v.Set(node.PasskeyRemove, "666f6f666f6f") + }, id) + + if spa { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + } else { + assert.Contains(t, res.Request.URL.String(), fix.uiTS.URL) + } + + t.Run("response", func(t *testing.T) { + assert.EqualValues(t, flow.StateShowForm, gjson.Get(body, "state").String(), body) + snapshotx.SnapshotTExcept(t, json.RawMessage(gjson.Get(body, "ui.nodes.#(attributes.name==passkey_remove)").String()), nil) + + actual, err := fix.reg.Persister().GetIdentityConfidential(fix.ctx, id.ID) + require.NoError(t, err) + cred, ok := actual.GetCredentials(identity.CredentialsTypePasskey) + assert.True(t, ok) + assert.Len(t, gjson.GetBytes(cred.Config, "credentials").Array(), 1) + }) + } + + t.Run("type=browser", func(t *testing.T) { + run(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true) + }) + }) + + t.Run("case=remove all passkeys", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + id := fix.createIdentity(t) + allCred, ok := id.GetCredentials(identity.CredentialsTypePasskey) + assert.True(t, ok) + + var cc identity.CredentialsWebAuthnConfig + require.NoError(t, json.Unmarshal(allCred.Config, &cc)) + require.Len(t, cc.Credentials, 2) + + for _, cred := range cc.Credentials { + body, res := doBrowserFlow(t, spa, func(v url.Values) { + v.Set(node.PasskeyRemove, fmt.Sprintf("%x", cred.ID)) + }, id) + + if spa { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + } else { + assert.Contains(t, res.Request.URL.String(), fix.uiTS.URL) + } + assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body) + } + + actual, err := fix.reg.Persister().GetIdentityConfidential(fix.ctx, id.ID) + require.NoError(t, err) + _, ok = actual.GetCredentials(identity.CredentialsTypePasskey) + assert.False(t, ok) + // Check not to remove other credentials with webauthn + _, ok = actual.GetCredentials(identity.CredentialsTypePassword) + assert.True(t, ok) + } + + t.Run("type=browser", func(t *testing.T) { + run(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true) + }) + }) + + t.Run("case=fails with browser submit register payload is invalid", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + id := fix.createIdentity(t) + body, res := doBrowserFlow(t, spa, func(v url.Values) { + v.Set(node.PasskeyRemove, fmt.Sprintf("%x", []byte("foofoo"))) + }, id) + + if spa { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + } else { + assert.Contains(t, res.Request.URL.String(), fix.uiTS.URL) + } + assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(body, "state").String())) + + actual, err := fix.reg.Persister().GetIdentityConfidential(fix.ctx, id.ID) + require.NoError(t, err) + cred, ok := actual.GetCredentials(identity.CredentialsTypePasskey) + assert.True(t, ok) + assert.Len(t, gjson.GetBytes(cred.Config, "credentials").Array(), 1) + assert.Equal(t, "bar", gjson.GetBytes(cred.Config, "credentials.0.display_name").String()) + } + + t.Run("type=browser", func(t *testing.T) { + run(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true) + }) + }) + + t.Run("case=is not responsible if neither remove or register is set", func(t *testing.T) { + run := func(t *testing.T, spa bool) { + id := fix.createIdentity(t) + body, res := doBrowserFlow(t, spa, func(v url.Values) { + v.Set(node.PasskeyRemove, "") + v.Set(node.PasskeyRegister, "") + }, id) + + if spa { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+settings.RouteSubmitFlow) + } else { + assert.Contains(t, res.Request.URL.String(), fix.uiTS.URL) + } + + assert.Equal(t, text.NewErrorValidationSettingsNoStrategyFound().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body) + } + + t.Run("type=browser", func(t *testing.T) { + run(t, false) + }) + + t.Run("type=spa", func(t *testing.T) { + run(t, true) + }) + }) + + t.Run("case=should fail if no identifier was set in the schema", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://stub/missing-identifier.schema.json") + + for _, f := range []string{"spa", "browser"} { + t.Run("type="+f, func(t *testing.T) { + isSPA := f == "spa" + + var id identity.Identity + require.NoError(t, json.Unmarshal(settingsFixtureSuccessIdentity, &id)) + _ = fix.reg.PrivilegedIdentityPool().DeleteIdentity(fix.ctx, id.ID) + id.NID = x.NewUUID() + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, &id) + + req, err := http.NewRequest("GET", fix.publicTS.URL+settings.RouteInitBrowserFlow, nil) + require.NoError(t, err) + if isSPA { + req.Header.Set("Accept", "application/json") + } + res, err := browserClient.Do(req) + require.NoError(t, err) + + actual := x.MustReadAll(res.Body) + defer res.Body.Close() + + assert.Equal(t, text.NewErrorValidationIdentifierMissing().Text, gjson.GetBytes(actual, "ui.messages.0.text").String(), "%s", actual) + }) + } + }) +} diff --git a/selfservice/strategy/passkey/passkey_strategy.go b/selfservice/strategy/passkey/passkey_strategy.go new file mode 100644 index 000000000000..53102ae48982 --- /dev/null +++ b/selfservice/strategy/passkey/passkey_strategy.go @@ -0,0 +1,119 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey + +import ( + "context" + "encoding/json" + "strings" + + "github.com/pkg/errors" + + "github.com/ory/kratos/continuity" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hash" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/session" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/decoderx" +) + +type strategyDependencies interface { + x.LoggingProvider + x.WriterProvider + x.CSRFTokenGeneratorProvider + x.CSRFProvider + x.TracingProvider + + config.Provider + + continuity.ManagementProvider + + errorx.ManagementProvider + hash.HashProvider + + registration.HandlerProvider + registration.HooksProvider + registration.ErrorHandlerProvider + registration.HookExecutorProvider + registration.FlowPersistenceProvider + + login.HooksProvider + login.ErrorHandlerProvider + login.HookExecutorProvider + login.FlowPersistenceProvider + login.HandlerProvider + + settings.FlowPersistenceProvider + settings.HookExecutorProvider + settings.HooksProvider + settings.ErrorHandlerProvider + + identity.PrivilegedPoolProvider + identity.ValidationProvider + identity.ActiveCredentialsCounterStrategyProvider + identity.ManagementProvider + + session.HandlerProvider + session.ManagementProvider +} + +var ( + _ login.Strategy = new(Strategy) + _ registration.Strategy = new(Strategy) + _ identity.ActiveCredentialsCounter = new(Strategy) +) + +type Strategy struct { + d strategyDependencies + hd *decoderx.HTTP +} + +func NewStrategy(d any) *Strategy { + return &Strategy{ + d: d.(strategyDependencies), + hd: decoderx.NewHTTP(), + } +} + +func (*Strategy) ID() identity.CredentialsType { + return identity.CredentialsTypePasskey +} + +func (*Strategy) NodeGroup() node.UiNodeGroup { + return node.PasskeyGroup +} + +func (s *Strategy) CompletedAuthenticationMethod(context.Context) session.AuthenticationMethod { + return session.AuthenticationMethod{ + Method: identity.CredentialsTypePasskey, + AAL: identity.AuthenticatorAssuranceLevel1, + } +} + +func (s *Strategy) CountActiveMultiFactorCredentials(_ context.Context, _ map[identity.CredentialsType]identity.Credentials) (count int, err error) { + return 0, nil +} + +func (s *Strategy) CountActiveFirstFactorCredentials(_ context.Context, cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { + return s.countCredentials(cc) +} + +func (s *Strategy) countCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { + for _, c := range cc { + if c.Type == s.ID() && len(c.Config) > 0 && len(strings.Join(c.Identifiers, "")) > 0 { + var conf identity.CredentialsWebAuthnConfig + if err = json.Unmarshal(c.Config, &conf); err != nil { + return 0, errors.WithStack(err) + } + count += len(conf.Credentials) + } + } + return +} diff --git a/selfservice/strategy/passkey/schema.go b/selfservice/strategy/passkey/schema.go new file mode 100644 index 000000000000..8be1150729bc --- /dev/null +++ b/selfservice/strategy/passkey/schema.go @@ -0,0 +1,15 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey + +import _ "embed" + +//go:embed .schema/registration.schema.json +var registrationSchema []byte + +//go:embed .schema/login.schema.json +var loginSchema []byte + +//go:embed .schema/settings.schema.json +var settingsSchema []byte diff --git a/selfservice/strategy/passkey/stub/login.schema.json b/selfservice/strategy/passkey/stub/login.schema.json new file mode 100644 index 000000000000..d6e72ec5fb0f --- /dev/null +++ b/selfservice/strategy/passkey/stub/login.schema.json @@ -0,0 +1,31 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "website": { + "type": "string" + }, + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "passkey": { + "display_name": true + } + } + } + } + }, + "required": [] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/login_webauthn.schema.json b/selfservice/strategy/passkey/stub/login_webauthn.schema.json new file mode 100644 index 000000000000..a94dfcc80d6d --- /dev/null +++ b/selfservice/strategy/passkey/stub/login_webauthn.schema.json @@ -0,0 +1,31 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "website": { + "type": "string" + }, + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + } + } + } + } + }, + "required": [] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/missing-identifier.schema.json b/selfservice/strategy/passkey/stub/missing-identifier.schema.json new file mode 100644 index 000000000000..43565799bba7 --- /dev/null +++ b/selfservice/strategy/passkey/stub/missing-identifier.schema.json @@ -0,0 +1,21 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + } + }, + "required": [ + "email" + ] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/noid.schema.json b/selfservice/strategy/passkey/stub/noid.schema.json new file mode 100644 index 000000000000..d1dcaa77d138 --- /dev/null +++ b/selfservice/strategy/passkey/stub/noid.schema.json @@ -0,0 +1,18 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + } + } + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/profile.schema.json b/selfservice/strategy/passkey/stub/profile.schema.json new file mode 100644 index 000000000000..2503b33597d7 --- /dev/null +++ b/selfservice/strategy/passkey/stub/profile.schema.json @@ -0,0 +1,25 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "ory.sh/kratos": { + "credentials": { + "passkey": { + "identifier": true + } + } + } + } + } + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/registration.schema.json b/selfservice/strategy/passkey/stub/registration.schema.json new file mode 100644 index 000000000000..441a72e44fcc --- /dev/null +++ b/selfservice/strategy/passkey/stub/registration.schema.json @@ -0,0 +1,35 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "foobar": { + "type": "string", + "minLength": 2 + }, + "username": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "passkey": { + "display_name": true + }, + "webauthn": { + "identifier": true + } + } + } + } + }, + "required": ["foobar", "username"] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/stub/settings.schema.json b/selfservice/strategy/passkey/stub/settings.schema.json new file mode 100644 index 000000000000..d6e72ec5fb0f --- /dev/null +++ b/selfservice/strategy/passkey/stub/settings.schema.json @@ -0,0 +1,31 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "website": { + "type": "string" + }, + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "passkey": { + "display_name": true + } + } + } + } + }, + "required": [] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/passkey/testfixture_test.go b/selfservice/strategy/passkey/testfixture_test.go new file mode 100644 index 000000000000..3f0fadfd2387 --- /dev/null +++ b/selfservice/strategy/passkey/testfixture_test.go @@ -0,0 +1,383 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package passkey_test + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/sjson" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + kratos "github.com/ory/kratos/internal/httpclient" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/sqlxx" +) + +func newRegistrationRegistry(t *testing.T) *driver.RegistryDefault { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", true) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, true) + enablePasskeyStrategy(conf) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationLoginHints, true) + return reg +} + +func newLoginRegistry(t *testing.T) *driver.RegistryDefault { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", true) + enablePasskeyStrategy(conf) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationLoginHints, true) + return reg +} + +func enablePasskeyStrategy(conf *config.Config) { + ctx := context.Background() + key := config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypePasskey) + conf.MustSet(ctx, key+".enabled", true) + conf.MustSet(ctx, key+".config.rp.display_name", "Ory Corp") + conf.MustSet(ctx, key+".config.rp.id", "localhost") + conf.MustSet(ctx, key+".config.rp.origins", []string{"http://localhost:4455"}) +} + +type fixture struct { + ctx context.Context + conf *config.Config + reg *driver.RegistryDefault + + publicTS *httptest.Server + redirTS *httptest.Server + redirNoSessionTS *httptest.Server + uiTS *httptest.Server + errTS *httptest.Server + loginTS *httptest.Server +} + +func newRegistrationFixture(t *testing.T) *fixture { + fix := new(fixture) + fix.ctx = context.Background() + fix.reg = newRegistrationRegistry(t) + fix.conf = fix.reg.Config() + ctx := fix.ctx + + router := x.NewRouterPublic() + fix.publicTS, _ = testhelpers.NewKratosServerWithRouters(t, fix.reg, router, x.NewRouterAdmin()) + + _ = testhelpers.NewErrorTestServer(t, fix.reg) + _ = testhelpers.NewRegistrationUIFlowEchoServer(t, fix.reg) + _ = testhelpers.NewRedirSessionEchoTS(t, fix.reg) + + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/registration.schema.json") + fix.conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"}) + + fix.redirTS = testhelpers.NewRedirSessionEchoTS(t, fix.reg) + fix.redirNoSessionTS = testhelpers.NewRedirNoSessionTS(t, fix.reg) + + fix.useReturnToFromTS(fix.redirTS) + + return fix +} + +func newLoginFixture(t *testing.T) *fixture { + fix := new(fixture) + fix.ctx = context.Background() + fix.reg = newLoginRegistry(t) + fix.conf = fix.reg.Config() + ctx := fix.ctx + + fix.conf.MustSet(ctx, + config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", + false) + + router := x.NewRouterPublic() + fix.publicTS, _ = testhelpers.NewKratosServerWithRouters(t, fix.reg, router, x.NewRouterAdmin()) + + fix.errTS = testhelpers.NewErrorTestServer(t, fix.reg) + fix.uiTS = testhelpers.NewLoginUIFlowEchoServer(t, fix.reg) + fix.loginTS = fix.uiTS + + // Overwrite these two to make it more explicit when tests fail + fix.conf.MustSet(ctx, config.ViperKeySelfServiceErrorUI, fix.errTS.URL+"/error-ts") + fix.conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, fix.uiTS.URL+"/login-ts") + + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/login.schema.json") + fix.conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"}) + + fix.redirTS = testhelpers.NewRedirSessionEchoTS(t, fix.reg) + fix.redirNoSessionTS = testhelpers.NewRedirNoSessionTS(t, fix.reg) + + fix.useReturnToFromTS(fix.redirTS) + + return fix +} + +func newSettingsFixture(t *testing.T) *fixture { + fix := newLoginFixture(t) + fix.uiTS = testhelpers.NewSettingsUIFlowEchoServer(t, fix.reg) + fix.conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1m") + testhelpers.SetDefaultIdentitySchema(fix.conf, "file://./stub/settings.schema.json") + fix.conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"}) + fix.conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, "aal1") + fix.conf.MustSet(fix.ctx, config.ViperKeySessionWhoAmIAAL, "aal1") + fix.conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".profile.enabled", false) + + return fix +} + +func (fix *fixture) checkURL(t *testing.T, shouldRedirect bool, res *http.Response) { + if shouldRedirect { + assert.Contains(t, res.Request.URL.String(), fix.uiTS.URL+"/login-ts") + } else { + assert.Contains(t, res.Request.URL.String(), fix.publicTS.URL+login.RouteSubmitFlow) + } +} + +func (fix *fixture) loginViaAPI(t *testing.T, v func(url.Values), apiClient *http.Client, opts ...testhelpers.InitFlowWithOption) (string, *http.Response) { + f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, fix.publicTS, false, opts...) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + v(values) + payload := testhelpers.EncodeFormAsJSON(t, true, values) + return testhelpers.LoginMakeRequest(t, true, false, f, apiClient, payload) +} + +func (fix *fixture) loginViaBrowser(t *testing.T, spa bool, cb func(url.Values), browserClient *http.Client, opts ...testhelpers.InitFlowWithOption) (string, *http.Response) { + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, fix.publicTS, false, spa, false, false, opts...) + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + cb(values) + return testhelpers.LoginMakeRequest(t, false, spa, f, browserClient, values.Encode()) +} + +func (fix *fixture) createIdentityWithPasskey(t *testing.T, c identity.Credentials) *identity.Identity { + var id identity.Identity + require.NoError(t, json.Unmarshal(loginSuccessIdentity, &id)) + + id.SetCredentials(identity.CredentialsTypePasskey, identity.Credentials{ + Identifiers: []string{"some-random-user-handle"}, + Config: c.Config, + Type: identity.CredentialsTypePasskey, + Version: c.Version, + }) + + // We clean up the identity in case it has been created before + _ = fix.reg.PrivilegedIdentityPool().DeleteIdentity(fix.ctx, id.ID) + + require.NoError(t, fix.reg.PrivilegedIdentityPool().CreateIdentity(fix.ctx, &id)) + + return &id +} + +func (fix *fixture) submitWebAuthnLoginFlowWithClient(t *testing.T, isSPA bool, f *kratos.LoginFlow, contextFixture []byte, client *http.Client, cb func(values url.Values)) (string, *http.Response, *kratos.LoginFlow) { + // We inject the session to replay + interim, err := fix.reg.LoginFlowPersister().GetLoginFlow(fix.ctx, uuid.FromStringOrNil(f.Id)) + require.NoError(t, err) + interim.InternalContext = contextFixture + require.NoError(t, fix.reg.LoginFlowPersister().UpdateLoginFlow(fix.ctx, interim)) + + values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) + cb(values) + + // We use the response replay + body, res := testhelpers.LoginMakeRequest(t, false, isSPA, f, client, values.Encode()) + return body, res, f +} + +func (fix *fixture) submitWebAuthnLoginWithClient(t *testing.T, isSPA bool, contextFixture []byte, client *http.Client, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.LoginFlow) { + f := testhelpers.InitializeLoginFlowViaBrowser(t, client, fix.publicTS, false, isSPA, false, false, opts...) + return fix.submitWebAuthnLoginFlowWithClient(t, isSPA, f, contextFixture, client, cb) +} + +func (fix *fixture) submitWebAuthnLogin(t *testing.T, ctx context.Context, isSPA bool, id *identity.Identity, contextFixture []byte, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.LoginFlow) { + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, id) + return fix.submitWebAuthnLoginWithClient(t, isSPA, contextFixture, browserClient, cb, opts...) +} + +// useReturnToFromTS sets the "return to" server, which will assert the session +// state (redirTS: enforce that a session exists, redirNoSessionTS: enforce that +// no session exists) +func (fix *fixture) useReturnToFromTS(ts *httptest.Server) { + fix.conf.MustSet(fix.ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, ts.URL+"/default-return-to") + fix.conf.MustSet(fix.ctx, config.ViperKeySelfServiceRegistrationAfter+"."+config.DefaultBrowserReturnURL, ts.URL+"/registration-return-ts") +} +func (fix *fixture) useRedirTS() { fix.useReturnToFromTS(fix.redirTS) } +func (fix *fixture) useRedirNoSessionTS() { fix.useReturnToFromTS(fix.redirNoSessionTS) } + +func (fix *fixture) disableSessionAfterRegistration() { + fix.conf.MustSet(fix.ctx, config.HookStrategyKey( + config.ViperKeySelfServiceRegistrationAfter, + identity.CredentialsTypePasskey.String(), + ), nil) +} +func (fix *fixture) enableSessionAfterRegistration() { + fix.conf.MustSet(fix.ctx, config.HookStrategyKey( + config.ViperKeySelfServiceRegistrationAfter, + identity.CredentialsTypePasskey.String(), + ), []config.SelfServiceHook{{Name: "session"}}) +} + +type submitPasskeyOpt struct { + initFlowOpts []testhelpers.InitFlowWithOption + userID string + internalContext sqlxx.JSONRawMessage +} + +type submitPasskeyOption func(o *submitPasskeyOpt) + +func withUserID(id string) submitPasskeyOption { + return func(o *submitPasskeyOpt) { + o.userID = base64.StdEncoding.EncodeToString([]byte(id)) + } +} + +func withInternalContext(ic sqlxx.JSONRawMessage) submitPasskeyOption { + return func(o *submitPasskeyOpt) { + o.internalContext = ic + } +} + +func (fix *fixture) submitPasskeyBrowserRegistration( + t *testing.T, + flowType string, + client *http.Client, + cb func(values url.Values), + opts ...submitPasskeyOption, +) (string, *http.Response, *kratos.RegistrationFlow) { + return fix.submitPasskeyRegistration(t, flowType, client, cb, append([]submitPasskeyOption{withInternalContext(registrationFixtureSuccessBrowserInternalContext)}, opts...)...) +} + +func (fix *fixture) submitPasskeyAndroidRegistration( + t *testing.T, + flowType string, + client *http.Client, + cb func(values url.Values), + opts ...submitPasskeyOption, +) (string, *http.Response, *kratos.RegistrationFlow) { + return fix.submitPasskeyRegistration(t, flowType, client, cb, + append([]submitPasskeyOption{withInternalContext( + registrationFixtureSuccessAndroidInternalContext, + )}, opts...)...) +} + +func (fix *fixture) submitPasskeyRegistration( + t *testing.T, + flowType string, + client *http.Client, + cb func(values url.Values), + opts ...submitPasskeyOption, +) (string, *http.Response, *kratos.RegistrationFlow) { + o := &submitPasskeyOpt{} + for _, fn := range opts { + fn(o) + } + + isSPA := flowType == "spa" + regFlow := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, fix.publicTS, isSPA, false, false, o.initFlowOpts...) + + // First step: fill out traits and click on "sign up with passkey" + values := testhelpers.SDKFormFieldsToURLValues(regFlow.Ui.Nodes) + cb(values) + passkeyRegisterVal := values.Get(node.PasskeyRegister) // needed in the second step + values.Del(node.PasskeyRegister) + values.Set("method", "passkey") + body, _ := testhelpers.RegistrationMakeRequest(t, false, isSPA, regFlow, client, values.Encode()) + + // We inject the session to replay + interim, err := fix.reg.RegistrationFlowPersister().GetRegistrationFlow(fix.ctx, uuid.FromStringOrNil(regFlow.Id)) + require.NoError(t, err) + interim.InternalContext = o.internalContext + if o.userID != "" { + interim.InternalContext, err = sjson.SetBytes(interim.InternalContext, "passkey_session_data.user_id", o.userID) + require.NoError(t, err) + } + require.NoError(t, fix.reg.RegistrationFlowPersister().UpdateRegistrationFlow(fix.ctx, interim)) + + // Second step: fill out passkey response + values.Set(node.PasskeyRegister, passkeyRegisterVal) + body, res := testhelpers.RegistrationMakeRequest(t, false, isSPA, regFlow, client, values.Encode()) + + return body, res, regFlow +} + +func (fix *fixture) makeRegistration(t *testing.T, flowType string, values func(v url.Values), opts ...submitPasskeyOption) (actual string, res *http.Response, fetchedFlow *registration.Flow) { + actual, res, actualFlow := fix.submitPasskeyBrowserRegistration(t, flowType, testhelpers.NewClientWithCookies(t), values, opts...) + fetchedFlow, err := fix.reg.RegistrationFlowPersister().GetRegistrationFlow(fix.ctx, uuid.FromStringOrNil(actualFlow.Id)) + require.NoError(t, err) + + return actual, res, fetchedFlow +} + +func (fix *fixture) makeSuccessfulRegistration(t *testing.T, flowType string, expectReturnTo string, values func(v url.Values), opts ...submitPasskeyOption) (actual string) { + actual, res, _ := fix.makeRegistration(t, flowType, values, opts...) + if flowType == "spa" { + expectReturnTo = fix.publicTS.URL + } + assert.Contains(t, res.Request.URL.String(), expectReturnTo, "%+v\n\t%s", res.Request, assertx.PrettifyJSONPayload(t, actual)) + return actual +} + +func (fix *fixture) createIdentityWithoutPasskey(t *testing.T) *identity.Identity { + id := fix.createIdentity(t) + delete(id.Credentials, identity.CredentialsTypePasskey) + require.NoError(t, fix.reg.PrivilegedIdentityPool().UpdateIdentity(fix.ctx, id)) + return id +} + +func (fix *fixture) createIdentityAndReturnIdentifier(t *testing.T, conf []byte) (*identity.Identity, string) { + identifier := x.NewUUID().String() + "@ory.sh" + password := x.NewUUID().String() + p, err := fix.reg.Hasher(fix.ctx).Generate(fix.ctx, []byte(password)) + require.NoError(t, err) + i := &identity.Identity{ + Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, identifier)), + VerifiableAddresses: []identity.VerifiableAddress{ + { + Value: identifier, + Verified: false, + CreatedAt: time.Now(), + }, + }, + } + if conf == nil { + conf = []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo"},{"id":"YmFyYmFy","display_name":"bar"}]}`) + } + require.NoError(t, fix.reg.PrivilegedIdentityPool().CreateIdentity(fix.ctx, i)) + i.Credentials = map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{identifier}, + Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`), + }, + identity.CredentialsTypePasskey: { + Type: identity.CredentialsTypePasskey, + Identifiers: []string{identifier}, + Config: conf, + }, + } + require.NoError(t, fix.reg.PrivilegedIdentityPool().UpdateIdentity(fix.ctx, i)) + return i, identifier +} + +func (fix *fixture) createIdentity(t *testing.T) *identity.Identity { + id, _ := fix.createIdentityAndReturnIdentifier(t, nil) + return id +} diff --git a/selfservice/strategy/password/.schema/login.schema.json b/selfservice/strategy/password/.schema/login.schema.json index 0ceccbc72738..67b1252c0ec0 100644 --- a/selfservice/strategy/password/.schema/login.schema.json +++ b/selfservice/strategy/password/.schema/login.schema.json @@ -16,6 +16,10 @@ }, "method": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } }, "allOf": [ diff --git a/selfservice/strategy/password/.schema/settings.schema.json b/selfservice/strategy/password/.schema/settings.schema.json index 442efe38b2a7..5e7b66784a69 100644 --- a/selfservice/strategy/password/.schema/settings.schema.json +++ b/selfservice/strategy/password/.schema/settings.schema.json @@ -15,6 +15,10 @@ "password": { "type": "string", "minLength": 1 + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json new file mode 100644 index 000000000000..4b13f4012f6f --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json @@ -0,0 +1,74 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "text", + "value": "", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json new file mode 100644 index 000000000000..eb9d0e213786 --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json @@ -0,0 +1,67 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "some@user.com", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010001, + "text": "Sign in", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_disabled.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_disabled.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_enabled.json new file mode 100644 index 000000000000..831d9f07ba25 --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_enabled.json @@ -0,0 +1,54 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_password.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_password.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_password.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_password.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_password.json new file mode 100644 index 000000000000..831d9f07ba25 --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_password.json @@ -0,0 +1,54 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled_and_identity_has_no_password.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled_and_identity_has_no_password.json new file mode 100644 index 000000000000..831d9f07ba25 --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled_and_identity_has_no_password.json @@ -0,0 +1,54 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_disabled.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_disabled.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_enabled.json new file mode 100644 index 000000000000..831d9f07ba25 --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_enabled.json @@ -0,0 +1,54 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json new file mode 100644 index 000000000000..19765bd501b6 --- /dev/null +++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json @@ -0,0 +1 @@ +null diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go index 0879c47517ca..c64680592a14 100644 --- a/selfservice/strategy/password/login.go +++ b/selfservice/strategy/password/login.go @@ -10,32 +10,36 @@ import ( "net/http" "time" - "github.com/ory/kratos/selfservice/flowhelpers" - "github.com/ory/kratos/session" + "go.opentelemetry.io/otel/attribute" - "github.com/ory/x/stringsx" + "github.com/ory/x/otelx" "github.com/gofrs/uuid" - "github.com/pkg/errors" "github.com/ory/herodot" - "github.com/ory/x/decoderx" - "github.com/ory/kratos/hash" "github.com/ory/kratos/identity" "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flowhelpers" + "github.com/ory/kratos/selfservice/hook" + "github.com/ory/kratos/selfservice/strategy/idfirst" + "github.com/ory/kratos/session" "github.com/ory/kratos/text" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" + "github.com/ory/x/decoderx" + "github.com/ory/x/stringsx" ) +var _ login.FormHydrator = new(Strategy) + func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) { } -func (s *Strategy) handleLoginError(w http.ResponseWriter, r *http.Request, f *login.Flow, payload *updateLoginFlowWithPasswordMethod, err error) error { +func (s *Strategy) handleLoginError(r *http.Request, f *login.Flow, payload updateLoginFlowWithPasswordMethod, err error) error { if f != nil { f.UI.Nodes.ResetNodes("password") f.UI.Nodes.SetValueAttribute("identifier", stringsx.Coalesce(payload.Identifier, payload.LegacyIdentifier)) @@ -48,7 +52,11 @@ func (s *Strategy) handleLoginError(w http.ResponseWriter, r *http.Request, f *l } func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, _ *session.Session) (i *identity.Identity, err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.password.strategy.Login") + defer otelx.End(span, &err) + if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil { + span.SetAttributes(attribute.String("not_responsible_reason", "requested AAL is not AAL1")) return nil, err } @@ -61,11 +69,12 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, decoderx.HTTPDecoderSetValidatePayloads(true), decoderx.MustHTTPRawJSONSchemaCompiler(loginSchema), decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { - return nil, s.handleLoginError(w, r, f, &p, err) + return nil, s.handleLoginError(r, f, p, err) } + f.TransientPayload = p.TransientPayload - if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { - return nil, s.handleLoginError(w, r, f, &p, err) + if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + return nil, s.handleLoginError(r, f, p, err) } // Reduce possibility of DDoS attacks with long password @@ -74,10 +83,11 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError())) } - i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), s.ID(), stringsx.Coalesce(p.Identifier, p.LegacyIdentifier)) + identifier := stringsx.Coalesce(p.Identifier, p.LegacyIdentifier) + i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, s.ID(), identifier) if err != nil { - time.Sleep(x.RandomDelay(s.d.Config().HasherArgon2(r.Context()).ExpectedDuration, s.d.Config().HasherArgon2(r.Context()).ExpectedDeviation)) - return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError())) + time.Sleep(x.RandomDelay(s.d.Config().HasherArgon2(ctx).ExpectedDuration, s.d.Config().HasherArgon2(ctx).ExpectedDeviation)) + return nil, s.handleLoginError(r, f, p, errors.WithStack(schema.NewInvalidCredentialsError())) } var o identity.CredentialsPassword @@ -85,28 +95,48 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, if err := d.Decode(&o); err != nil { return nil, herodot.ErrInternalServerError.WithReason("The password credentials could not be decoded properly").WithDebug(err.Error()).WithWrap(err) } - //fandom-start - pass additional param: (identityId) i.ID - if err := hash.Compare(r.Context(), s.d.Config(), i.ID, []byte(p.Password), []byte(o.HashedPassword)); err != nil { - //fandom-end - return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError())) - } - if !s.d.Hasher(r.Context()).Understands([]byte(o.HashedPassword)) { - if err := s.migratePasswordHash(r.Context(), i.ID, []byte(p.Password)); err != nil { - return nil, s.handleLoginError(w, r, f, &p, err) + if o.ShouldUsePasswordMigrationHook() { + pwHook := s.d.Config().PasswordMigrationHook(ctx) + if !pwHook.Enabled { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Password migration hook is not enabled but password migration is requested.")) + } + + migrationHook := hook.NewPasswordMigrationHook(s.d, pwHook.Config) + err = migrationHook.Execute(ctx, &hook.PasswordMigrationRequest{Identifier: identifier, Password: p.Password}) + if err != nil { + return nil, s.handleLoginError(r, f, p, err) + } + + if err := s.migratePasswordHash(ctx, i.ID, []byte(p.Password)); err != nil { + return nil, s.handleLoginError(r, f, p, err) + } + } else { + //fandom-start - pass additional param: (identityId) i.ID + if err := hash.Compare(r.Context(), s.d.Config(), i.ID, []byte(p.Password), []byte(o.HashedPassword)); err != nil { + //fandom-end + return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError())) + } + + if !s.d.Hasher(ctx).Understands([]byte(o.HashedPassword)) { + if err := s.migratePasswordHash(ctx, i.ID, []byte(p.Password)); err != nil { + return nil, s.handleLoginError(r, f, p, err) + } } } - f.Active = identity.CredentialsTypePassword f.Active = s.ID() - if err = s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), f); err != nil { - return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error()))) + if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { + return nil, s.handleLoginError(r, f, p, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error()))) } return i, nil } -func (s *Strategy) migratePasswordHash(ctx context.Context, identifier uuid.UUID, password []byte) error { +func (s *Strategy) migratePasswordHash(ctx context.Context, identifier uuid.UUID, password []byte) (err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.password.strategy.migratePasswordHash") + defer otelx.End(span, &err) + hpw, err := s.d.Hasher(ctx).Generate(ctx, password) if err != nil { return err @@ -129,46 +159,97 @@ func (s *Strategy) migratePasswordHash(ctx context.Context, identifier uuid.UUID c.Config = co i.SetCredentials(s.ID(), *c) - return s.d.PrivilegedIdentityPool().UpdateIdentity(ctx, i) + return s.d.IdentityManager().Update(ctx, i, identity.ManagerAllowWriteProtectedTraits) } -func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *login.Flow) error { - // This strategy can only solve AAL1 - if requestedAAL > identity.AuthenticatorAssuranceLevel1 { +func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, sr *login.Flow) (err error) { + ctx := r.Context() + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.password.strategy.PopulateLoginMethodFirstFactorRefresh") + defer otelx.End(span, &err) + + identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID()) + if identifier == "" { return nil } - if sr.IsForced() { - // We only show this method on a refresh request if the user has indeed a password set. - identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID()) - if identifier == "" { - return nil - } + // If we don't have a password set, do not show the password field. + count, err := s.CountActiveFirstFactorCredentials(ctx, id.Credentials) + if err != nil { + return err + } else if count == 0 { + return nil + } - count, err := s.CountActiveFirstFactorCredentials(id.Credentials) - if err != nil { + sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden)) + sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword)) + sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLogin())) + return nil +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *login.Flow) error { + return nil +} + +func (s *Strategy) addIdentifierNode(r *http.Request, sr *login.Flow) error { + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) + if err != nil { + return err + } + + sr.UI.SetNode(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel)) + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error { + if err := s.addIdentifierNode(r, sr); err != nil { + return err + } + + sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword)) + sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLoginPassword())) + return nil +} + +func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, sr *login.Flow, opts ...login.FormHydratorModifier) (err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.password.strategy.PopulateLoginMethodIdentifierFirstCredentials") + defer otelx.End(span, &err) + + o := login.NewFormHydratorOptions(opts) + + var count int + if o.IdentityHint != nil { + var err error + // If we have an identity hint we can perform identity credentials discovery and + // hide this credential if it should not be included. + if count, err = s.CountActiveFirstFactorCredentials(ctx, o.IdentityHint.Credentials); err != nil { return err - } else if count == 0 { - return nil } + } + if count > 0 || s.d.Config().SecurityAccountEnumerationMitigate(ctx) { sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden)) - } else { - ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) - if err != nil { - return err - } - identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) - if err != nil { - return err - } - sr.UI.SetNode(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel)) + sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword)) + sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLoginPassword())) } - sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword, s.d.Config().PasswordPolicyConfig(r.Context()))) - sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLogin())) + if count == 0 { + return errors.WithStack(idfirst.ErrNoCredentialsFound) + } + + return nil +} +func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, sr *login.Flow) error { return nil } diff --git a/selfservice/strategy/password/login_test.go b/selfservice/strategy/password/login_test.go index 8d8879cce91c..c955bf7d8a20 100644 --- a/selfservice/strategy/password/login_test.go +++ b/selfservice/strategy/password/login_test.go @@ -11,48 +11,52 @@ import ( "fmt" "io" "net/http" + "net/http/httptest" "net/url" "strings" "testing" "time" + "github.com/ory/kratos/selfservice/strategy/idfirst" + + configtesthelpers "github.com/ory/kratos/driver/config/testhelpers" + + "github.com/ory/x/snapshotx" + "github.com/ory/kratos/driver" "github.com/ory/kratos/internal/registrationhelpers" "github.com/ory/kratos/selfservice/flow" "github.com/gofrs/uuid" - - "github.com/ory/x/urlx" - - "github.com/ory/kratos/hash" - kratos "github.com/ory/kratos/internal/httpclient" - "github.com/ory/x/assertx" - "github.com/ory/x/errorsx" - "github.com/ory/x/ioutilx" - "github.com/ory/x/sqlxx" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/hash" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" + kratos "github.com/ory/kratos/internal/httpclient" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/text" "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/errorsx" + "github.com/ory/x/ioutilx" + "github.com/ory/x/sqlxx" + "github.com/ory/x/urlx" ) //go:embed stub/login.schema.json var loginSchema []byte -func createIdentity(ctx context.Context, reg *driver.RegistryDefault, t *testing.T, identifier, password string) { +func createIdentity(ctx context.Context, reg *driver.RegistryDefault, t *testing.T, identifier, password string) *identity.Identity { p, _ := reg.Hasher(ctx).Generate(context.Background(), []byte(password)) iId := x.NewUUID() - require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &identity.Identity{ + id := &identity.Identity{ ID: iId, Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)), Credentials: map[identity.CredentialsType]identity.Credentials{ @@ -71,7 +75,9 @@ func createIdentity(ctx context.Context, reg *driver.RegistryDefault, t *testing IdentityID: iId, }, }, - })) + } + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, id)) + return id } func TestCompleteLogin(t *testing.T) { @@ -90,7 +96,11 @@ func TestCompleteLogin(t *testing.T) { conf.MustSet(ctx, config.ViperKeySelfServiceErrorUI, errTS.URL+"/error-ts") conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, uiTS.URL+"/login-ts") - testhelpers.SetDefaultIdentitySchemaFromRaw(conf, loginSchema) + testhelpers.SetIdentitySchemas(t, conf, map[string]string{ + "migration": "file://./stub/migration.schema.json", + "default": "file://./stub/login.schema.json", + }) + conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"}) ensureFieldsExist := func(t *testing.T, body []byte) { @@ -513,8 +523,9 @@ func TestCompleteLogin(t *testing.T) { }) t.Run("do not show password method if identity has no password set", func(t *testing.T) { - id := identity.NewIdentity("") - browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + id := identity.NewIdentity("default") + id.NID = x.NewUUID() + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) res, err := browserClient.Get(publicTS.URL + login.RouteInitBrowserFlow + "?refresh=true") require.NoError(t, err) @@ -573,8 +584,9 @@ func TestCompleteLogin(t *testing.T) { }) t.Run("do not show password method if identity has no password set", func(t *testing.T) { - id := identity.NewIdentity("") - hc := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + id := identity.NewIdentity("default") + id.NID = x.NewUUID() + hc := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) res, err := hc.Do(testhelpers.NewHTTPGetAJAXRequest(t, publicTS.URL+login.RouteInitBrowserFlow+"?refresh=true")) require.NoError(t, err) @@ -632,8 +644,9 @@ func TestCompleteLogin(t *testing.T) { }) t.Run("do not show password method if identity has no password set", func(t *testing.T) { - id := identity.NewIdentity("") - hc := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + id := identity.NewIdentity("default") + id.NID = x.NewUUID() + hc := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) res, err := hc.Do(testhelpers.NewHTTPGetAJAXRequest(t, publicTS.URL+login.RouteInitAPIFlow+"?refresh=true")) require.NoError(t, err) @@ -649,8 +662,6 @@ func TestCompleteLogin(t *testing.T) { }) t.Run("case=should return an error because not passing validation and reset previous errors and values", func(t *testing.T) { - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/login.schema.json") - check := func(t *testing.T, actual string) { assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+login.RouteSubmitFlow, "%s", actual) @@ -741,6 +752,32 @@ func TestCompleteLogin(t *testing.T) { assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body) }) + t.Run("should succeed and include redirect continue_with in SPA flow", func(t *testing.T) { + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, true, false, false) + values := url.Values{"method": {"password"}, "identifier": {strings.ToUpper(identifier)}, "password": {pwd}, "csrf_token": {x.FakeCSRFToken}}.Encode() + body, res := testhelpers.LoginMakeRequest(t, false, true, f, browserClient, values) + + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.EqualValues(t, conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), gjson.Get(body, "continue_with.0.redirect_browser_to").String(), "%s", body) + }) + + t.Run("should succeed and not have redirect continue_with in api flow", func(t *testing.T) { + identifier, pwd := x.NewUUID().String(), "password" + createIdentity(ctx, reg, t, identifier, pwd) + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false) + + body, res := testhelpers.LoginMakeRequest(t, true, true, f, browserClient, fmt.Sprintf(`{"method":"password","identifier":"%s","password":"%s"}`, strings.ToUpper(identifier), pwd)) + + assert.EqualValues(t, http.StatusOK, res.StatusCode, body) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) + }) + t.Run("should login even if old form field name is used", func(t *testing.T) { identifier, pwd := x.NewUUID().String(), "password" createIdentity(ctx, reg, t, identifier, pwd) @@ -807,7 +844,7 @@ func TestCompleteLogin(t *testing.T) { }) t.Run("should upgrade password not primary hashing algorithm", func(t *testing.T) { - identifier, pwd := x.NewUUID().String(), "password" + identifier, pwd := x.NewUUID().String()+"@google.com", "password" h := &hash.Pbkdf2{ Algorithm: "sha256", Iterations: 100000, @@ -818,8 +855,9 @@ func TestCompleteLogin(t *testing.T) { iId := x.NewUUID() require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &identity.Identity{ - ID: iId, - Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)), + ID: iId, + SchemaID: "migration", + Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, identifier)), Credentials: map[identity.CredentialsType]identity.Credentials{ identity.CredentialsTypePassword: { Type: identity.CredentialsTypePassword, @@ -849,7 +887,7 @@ func TestCompleteLogin(t *testing.T) { body := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, values, false, false, http.StatusOK, redirTS.URL) - assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body) + assert.Equal(t, identifier, gjson.Get(body, "identity.traits.email").String(), "%s", body) // check if password hash algorithm is upgraded _, c, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypePassword, identifier) @@ -862,6 +900,334 @@ func TestCompleteLogin(t *testing.T) { // retry after upgraded body = testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, values, false, true, http.StatusOK, redirTS.URL) - assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body) + assert.Equal(t, identifier, gjson.Get(body, "identity.traits.email").String(), "%s", body) + }) + + t.Run("suite=password migration hook", func(t *testing.T) { + ctx := context.Background() + + type ( + hookPayload = struct { + Identifier string `json:"identifier"` + Password string `json:"password"` + } + tsRequestHandler = func(hookPayload) (status int, body string) + ) + returnStatus := func(status int) func(string, string) tsRequestHandler { + return func(string, string) tsRequestHandler { + return func(hookPayload) (int, string) { return status, "" } + } + } + returnStatic := func(status int, body string) func(string, string) tsRequestHandler { + return func(string, string) tsRequestHandler { + return func(hookPayload) (int, string) { return status, body } + } + } + + // each test case sends (number of expected calls) handlers to the channel, at a max of 3 + tsChan := make(chan tsRequestHandler, 3) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + _ = r.Body.Close() + var payload hookPayload + require.NoError(t, json.Unmarshal(b, &payload)) + + select { + case handlerFn := <-tsChan: + status, body := handlerFn(payload) + w.WriteHeader(status) + _, _ = io.WriteString(w, body) + + default: + t.Fatal("unexpected call to the password migration hook") + } + })) + t.Cleanup(ts.Close) + + require.NoError(t, reg.Config().Set(ctx, config.ViperKeyPasswordMigrationHook, map[string]any{ + "config": map[string]any{"url": ts.URL}, + "enabled": true})) + + for _, tc := range []struct { + name string + hookHandler func(identifier, password string) tsRequestHandler + expectHookCalls int + setupFn func() func() + credentialsConfig string + expectSuccess bool + }{{ + name: "should call migration hook", + credentialsConfig: `{"use_password_migration_hook": true}`, + hookHandler: func(identifier, password string) tsRequestHandler { + return func(payload hookPayload) (status int, body string) { + if payload.Identifier == identifier && payload.Password == password { + return http.StatusOK, `{"status":"password_match"}` + } else { + return http.StatusOK, `{"status":"no_match"}` + } + } + }, + expectHookCalls: 1, + expectSuccess: true, + }, { + name: "should not update identity when the password is wrong", + credentialsConfig: `{"use_password_migration_hook": true}`, + hookHandler: returnStatus(http.StatusForbidden), + expectHookCalls: 1, + expectSuccess: false, + }, { + name: "should inspect response", + credentialsConfig: `{"use_password_migration_hook": true}`, + hookHandler: returnStatic(http.StatusOK, `{"status":"password_no_match"}`), + expectHookCalls: 1, + expectSuccess: false, + }, { + name: "should not update identity when the migration hook returns 200 without JSON", + credentialsConfig: `{"use_password_migration_hook": true}`, + hookHandler: returnStatus(http.StatusOK), + expectHookCalls: 1, + expectSuccess: false, + }, { + name: "should not update identity when the migration hook returns 500", + credentialsConfig: `{"use_password_migration_hook": true}`, + hookHandler: returnStatus(http.StatusInternalServerError), + expectHookCalls: 3, // expect retries on 500 + expectSuccess: false, + }, { + name: "should not update identity when the migration hook returns 201", + credentialsConfig: `{"use_password_migration_hook": true}`, + hookHandler: returnStatic(http.StatusCreated, `{"status":"password_match"}`), + expectHookCalls: 1, + expectSuccess: false, + }, { + name: "should not update identity and not call hook when hash is set", + credentialsConfig: `{"use_password_migration_hook": true, "hashed_password":"hash"}`, + expectSuccess: false, + }, { + name: "should not update identity and not call hook when use_password_migration_hook is not set", + credentialsConfig: `{"hashed_password":"hash"}`, + expectSuccess: false, + }, { + name: "should not update identity and not call hook when credential is empty", + credentialsConfig: `{}`, + expectSuccess: false, + }, { + name: "should not call migration hook if disabled", + credentialsConfig: `{"use_password_migration_hook": true}`, + setupFn: func() func() { + require.NoError(t, reg.Config().Set(ctx, config.ViperKeyPasswordMigrationHook+".enabled", false)) + return func() { + require.NoError(t, reg.Config().Set(ctx, config.ViperKeyPasswordMigrationHook+".enabled", true)) + } + }, + expectSuccess: false, + }} { + t.Run("case="+tc.name, func(t *testing.T) { + if tc.setupFn != nil { + cleanup := tc.setupFn() + t.Cleanup(cleanup) + } + + identifier := x.NewUUID().String() + "@google.com" + password := x.NewUUID().String() + iId := x.NewUUID() + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, &identity.Identity{ + ID: iId, + SchemaID: "migration", + Traits: identity.Traits(fmt.Sprintf(`{"email":"%s"}`, identifier)), + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: { + Type: identity.CredentialsTypePassword, + Identifiers: []string{identifier}, + Config: sqlxx.JSONRawMessage(tc.credentialsConfig), + }, + }, + VerifiableAddresses: []identity.VerifiableAddress{ + { + ID: x.NewUUID(), + Value: identifier, + Verified: true, + CreatedAt: time.Now(), + IdentityID: iId, + }, + }, + })) + + values := func(v url.Values) { + v.Set("identifier", identifier) + v.Set("method", identity.CredentialsTypePassword.String()) + v.Set("password", password) + } + + for range tc.expectHookCalls { + tsChan <- tc.hookHandler(identifier, password) + } + + browserClient := testhelpers.NewClientWithCookies(t) + + if tc.expectSuccess { + body := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, values, + false, false, http.StatusOK, redirTS.URL) + assert.Equal(t, identifier, gjson.Get(body, "identity.traits.email").String(), "%s", body) + + // check if password hash algorithm is upgraded + _, c, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, identity.CredentialsTypePassword, identifier) + require.NoError(t, err) + var o identity.CredentialsPassword + require.NoError(t, json.NewDecoder(bytes.NewBuffer(c.Config)).Decode(&o)) + assert.True(t, reg.Hasher(ctx).Understands([]byte(o.HashedPassword)), "%s", o.HashedPassword) + assert.True(t, hash.IsBcryptHash([]byte(o.HashedPassword)), "%s", o.HashedPassword) + + // retry after upgraded + body = testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, values, + false, true, http.StatusOK, redirTS.URL) + assert.Equal(t, identifier, gjson.Get(body, "identity.traits.email").String(), "%s", body) + } else { + body := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, values, + false, false, http.StatusOK, "") + assert.Empty(t, gjson.Get(body, "identity.traits.subject").String(), "%s", body) + // Check that the config did not change + _, c, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypePassword, identifier) + require.NoError(t, err) + assert.JSONEq(t, tc.credentialsConfig, string(c.Config)) + } + + // expect all hook calls to be done + select { + case <-tsChan: + t.Fatal("the test unexpectedly did too few calls to the password hook") + default: + // pass + } + }) + } + }) +} + +func TestFormHydration(t *testing.T) { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{"enabled": true}) + ctx = testhelpers.WithDefaultIdentitySchemaFromRaw(ctx, loginSchema) + + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypePassword) + require.NoError(t, err) + fh, ok := s.(login.FormHydrator) + require.True(t, ok) + + toSnapshot := func(t *testing.T, f *login.Flow) { + t.Helper() + // The CSRF token has a unique value that messes with the snapshot - ignore it. + f.UI.Nodes.ResetNodes("csrf_token") + snapshotx.SnapshotT(t, f.UI.Nodes) + } + newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) { + r := httptest.NewRequest("GET", "/self-service/login/browser", nil) + r = r.WithContext(ctx) + t.Helper() + f, err := login.NewFlow(conf, time.Minute, "csrf_token", r, flow.TypeBrowser) + require.NoError(t, err) + return r, f + } + + t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) { + r, f := newFlow(ctx, t) + f.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodFirstFactorRefresh", func(t *testing.T) { + r, f := newFlow(ctx, t) + id := createIdentity(ctx, reg, t, "some@user.com", "password") + r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + f.Refresh = true + require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodSecondFactorRefresh", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodSecondFactorRefresh(r, f)) + toSnapshot(t, f) + }) + + t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) { + t.Run("case=no options", func(t *testing.T) { + t.Run("case=account enumeration mitigation disabled", func(t *testing.T) { + ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false) + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=account enumeration mitigation enabled", func(t *testing.T) { + ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true) + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + }) + + t.Run("case=WithIdentifier", func(t *testing.T) { + t.Run("case=account enumeration mitigation disabled", func(t *testing.T) { + ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false) + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=account enumeration mitigation enabled", func(t *testing.T) { + ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true) + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + }) + + t.Run("case=WithIdentityHint", func(t *testing.T) { + t.Run("case=account enumeration mitigation enabled and identity has no password", func(t *testing.T) { + ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true) + + id := identity.NewIdentity("default") + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=account enumeration mitigation disabled", func(t *testing.T) { + ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false) + + t.Run("case=identity has password", func(t *testing.T) { + identifier, pwd := x.NewUUID().String(), "password" + id := createIdentity(ctx, reg, t, identifier, pwd) + + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id))) + toSnapshot(t, f) + }) + + t.Run("case=identity does not have a password", func(t *testing.T) { + id := identity.NewIdentity("default") + r, f := newFlow(ctx, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + }) + }) + }) + + t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) { + r, f := newFlow(ctx, t) + require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f)) + toSnapshot(t, f) }) } diff --git a/selfservice/strategy/password/op_helpers_test.go b/selfservice/strategy/password/op_helpers_test.go index ccbb186ae103..6a300807374c 100644 --- a/selfservice/strategy/password/op_helpers_test.go +++ b/selfservice/strategy/password/op_helpers_test.go @@ -70,21 +70,21 @@ type testConfig struct { browserClient *http.Client kratosPublicTS *httptest.Server clientAppTS *httptest.Server - hydraAdminClient *hydraclientgo.OAuth2ApiService + hydraAdminClient hydraclientgo.OAuth2API consentRemember bool requestedScope []string callTrace *[]callTrace } -func createHydraOAuth2ApiClient(url string) *hydraclientgo.OAuth2ApiService { +func createHydraOAuth2ApiClient(url string) hydraclientgo.OAuth2API { configuration := hydraclientgo.NewConfiguration() configuration.Host = urlx.ParseOrPanic(url).Host configuration.Servers = hydraclientgo.ServerConfigurations{{URL: url}} - return hydraclientgo.NewAPIClient(configuration).OAuth2Api + return hydraclientgo.NewAPIClient(configuration).OAuth2API } -func createOAuth2Client(t *testing.T, ctx context.Context, hydraAdmin *hydraclientgo.OAuth2ApiService, redirectURIs []string, scope string, skipConsent bool) string { +func createOAuth2Client(t *testing.T, ctx context.Context, hydraAdmin hydraclientgo.OAuth2API, redirectURIs []string, scope string, skipConsent bool) string { t.Helper() clientName := "kratos-hydra-integration-test-client-1" diff --git a/selfservice/strategy/password/op_login_test.go b/selfservice/strategy/password/op_login_test.go index 6d8492a2ff3e..1fad1cce1c18 100644 --- a/selfservice/strategy/password/op_login_test.go +++ b/selfservice/strategy/password/op_login_test.go @@ -227,7 +227,7 @@ func TestOAuth2Provider(t *testing.T) { t.Cleanup(func() { conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, true) }) - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier, pwd := x.NewUUID().String(), "password" createIdentity(ctx, reg, t, identifier, pwd) @@ -298,7 +298,7 @@ func TestOAuth2Provider(t *testing.T) { // to SessionPersistentCookie=false, the user is not // remembered from the previous OAuth2 flow. // The user must then re-authenticate and re-consent. - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier, pwd := x.NewUUID().String(), "password" createIdentity(ctx, reg, t, identifier, pwd) @@ -398,7 +398,7 @@ func TestOAuth2Provider(t *testing.T) { // The user must then only re-consent. conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, true) - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier, pwd := x.NewUUID().String(), "password" @@ -488,7 +488,7 @@ func TestOAuth2Provider(t *testing.T) { }) t.Run("should prompt login even with session with OAuth flow", func(t *testing.T) { - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier, pwd := x.NewUUID().String(), "password" createIdentity(ctx, reg, t, identifier, pwd) @@ -559,7 +559,7 @@ func TestOAuth2Provider(t *testing.T) { }) t.Run("first party clients can skip consent", func(t *testing.T) { - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier, pwd := x.NewUUID().String(), "password" createIdentity(ctx, reg, t, identifier, pwd) @@ -628,7 +628,7 @@ func TestOAuth2Provider(t *testing.T) { }) t.Run("oauth flow with consent remember, skips consent", func(t *testing.T) { - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier, pwd := x.NewUUID().String(), "password" createIdentity(ctx, reg, t, identifier, pwd) @@ -719,7 +719,7 @@ func TestOAuth2Provider(t *testing.T) { }) t.Run("should fail when Hydra session subject doesn't match the subject authenticated by Kratos", func(t *testing.T) { - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier, pwd := x.NewUUID().String(), "password" createIdentity(ctx, reg, t, identifier, pwd) diff --git a/selfservice/strategy/password/op_registration_test.go b/selfservice/strategy/password/op_registration_test.go index 7c0b05c81e5a..dc2df971b9f9 100644 --- a/selfservice/strategy/password/op_registration_test.go +++ b/selfservice/strategy/password/op_registration_test.go @@ -34,12 +34,13 @@ import ( func TestOAuth2ProviderRegistration(t *testing.T) { ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) + conf.MustSet(ctx, "selfservice.flows.registration.enable_legacy_one_step", true) kratosPublicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, x.NewRouterPublic(), x.NewRouterAdmin()) errTS := testhelpers.NewErrorTestServer(t, reg) redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) - var hydraAdminClient *hydraclientgo.OAuth2ApiService + var hydraAdminClient hydraclientgo.OAuth2API router := x.NewRouterPublic() @@ -260,7 +261,7 @@ func TestOAuth2ProviderRegistration(t *testing.T) { require.Equal(t, http.StatusOK, res.StatusCode) } - registerNewAccount := func(t *testing.T, ctx context.Context, browserClient *http.Client, identifier, password string) { + registerNewAccount := func(t *testing.T, browserClient *http.Client, identifier, password string) { // we need to create a new session directly with kratos f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, kratosPublicTS, false, false, false) require.NotNil(t, f) @@ -309,7 +310,7 @@ func TestOAuth2ProviderRegistration(t *testing.T) { Scopes: scopes, RedirectURL: clientAppTS.URL, } - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier := x.NewUUID().String() password := x.NewUUID().String() @@ -386,7 +387,7 @@ func TestOAuth2ProviderRegistration(t *testing.T) { RedirectURL: clientAppTS.URL, } - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier := x.NewUUID().String() password := x.NewUUID().String() @@ -417,7 +418,7 @@ func TestOAuth2ProviderRegistration(t *testing.T) { state: &clientAS, }) - registerNewAccount(t, ctx, browserClient, identifier, password) + registerNewAccount(t, browserClient, identifier, password) require.ElementsMatch(t, []callTrace{ RegistrationUI, @@ -471,7 +472,7 @@ func TestOAuth2ProviderRegistration(t *testing.T) { RedirectURL: clientAppTS.URL, } - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier := x.NewUUID().String() password := x.NewUUID().String() @@ -578,7 +579,7 @@ func TestOAuth2ProviderRegistration(t *testing.T) { RedirectURL: clientAppTS.URL, } - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier := x.NewUUID().String() password := x.NewUUID().String() @@ -658,7 +659,7 @@ func TestOAuth2ProviderRegistration(t *testing.T) { RedirectURL: clientAppTS.URL, } - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier := x.NewUUID().String() password := x.NewUUID().String() @@ -775,7 +776,7 @@ func TestOAuth2ProviderRegistration(t *testing.T) { RedirectURL: clientAppTS.URL, } - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) ct := make([]callTrace, 0) diff --git a/selfservice/strategy/password/registration.go b/selfservice/strategy/password/registration.go index 3d0accd6dd3a..0beb37a1351d 100644 --- a/selfservice/strategy/password/registration.go +++ b/selfservice/strategy/password/registration.go @@ -8,6 +8,8 @@ import ( "encoding/json" "net/http" + "github.com/ory/x/otelx" + "github.com/ory/kratos/text" "github.com/pkg/errors" @@ -50,19 +52,17 @@ type UpdateRegistrationFlowWithPasswordMethod struct { // Transient data to pass along to any webhooks // // required: false - TransientPayload json.RawMessage `json:"transient_payload,omitempty"` + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } -func (s *Strategy) RegisterRegistrationRoutes(_ *x.RouterPublic) { +func (s *Strategy) RegisterRegistrationRoutes(*x.RouterPublic) { } -func (s *Strategy) handleRegistrationError(_ http.ResponseWriter, r *http.Request, f *registration.Flow, p *UpdateRegistrationFlowWithPasswordMethod, err error) error { +func (s *Strategy) handleRegistrationError(r *http.Request, f *registration.Flow, p UpdateRegistrationFlowWithPasswordMethod, err error) error { if f != nil { - if p != nil { - for _, n := range container.NewFromJSON("", node.ProfileGroup, p.Traits, "traits").Nodes { - // we only set the value and not the whole field because we want to keep types from the initial form generation - f.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue()) - } + for _, n := range container.NewFromJSON("", node.ProfileGroup, p.Traits, "traits").Nodes { + // we only set the value and not the whole field because we want to keep types from the initial form generation + f.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue()) } if f.Type == flow.TypeBrowser { @@ -73,28 +73,31 @@ func (s *Strategy) handleRegistrationError(_ http.ResponseWriter, r *http.Reques return err } -func (s *Strategy) decode(p *UpdateRegistrationFlowWithPasswordMethod, r *http.Request) error { +func (s *Strategy) decode(p *UpdateRegistrationFlowWithPasswordMethod, r *http.Request) (err error) { return registration.DecodeBody(p, r, s.hd, s.d.Config(), registrationSchema) } -func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registration.Flow, i *identity.Identity) (err error) { +func (s *Strategy) Register(_ http.ResponseWriter, r *http.Request, f *registration.Flow, i *identity.Identity) (err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.password.strategy.Register") + defer otelx.End(span, &err) + if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.ID().String(), s.d); err != nil { return err } var p UpdateRegistrationFlowWithPasswordMethod if err := s.decode(&p, r); err != nil { - return s.handleRegistrationError(w, r, f, &p, err) + return s.handleRegistrationError(r, f, p, err) } f.TransientPayload = p.TransientPayload - if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { - return s.handleRegistrationError(w, r, f, &p, err) + if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + return s.handleRegistrationError(r, f, p, err) } if len(p.Password) == 0 { - return s.handleRegistrationError(w, r, f, &p, schema.NewRequiredError("#/password", "password")) + return s.handleRegistrationError(r, f, p, schema.NewRequiredError("#/password", "password")) } if len(p.Traits) == 0 { @@ -107,7 +110,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat defer close(hpw) defer close(errC) - h, err := s.d.Hasher(r.Context()).Generate(r.Context(), []byte(p.Password)) + h, err := s.d.Hasher(ctx).Generate(ctx, []byte(p.Password)) if err != nil { errC <- err return @@ -116,27 +119,27 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat }() if err != nil { - return s.handleRegistrationError(w, r, f, &p, err) + return s.handleRegistrationError(r, f, p, err) } i.Traits = identity.Traits(p.Traits) // We have to set the credential here, so the identity validator can populate the identifiers. // The password hash is computed in parallel and set later. if err := i.SetCredentialsWithConfig(s.ID(), identity.Credentials{Type: s.ID(), Identifiers: []string{}}, json.RawMessage("{}")); err != nil { - return s.handleRegistrationError(w, r, f, &p, err) + return s.handleRegistrationError(r, f, p, err) } - if err := s.validateCredentials(r.Context(), i, p.Password); err != nil { - return s.handleRegistrationError(w, r, f, &p, err) + if err := s.validateCredentials(ctx, i, p.Password); err != nil { + return s.handleRegistrationError(r, f, p, err) } select { case err := <-errC: - return s.handleRegistrationError(w, r, f, &p, err) + return s.handleRegistrationError(r, f, p, err) case h := <-hpw: co, err := json.Marshal(&identity.CredentialsPassword{HashedPassword: string(h)}) if err != nil { - return s.handleRegistrationError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode password options to JSON: %s", err))) + return s.handleRegistrationError(r, f, p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode password options to JSON: %s", err))) } i.UpsertCredentialsConfig(s.ID(), co, 0) } @@ -144,7 +147,10 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat return nil } -func (s *Strategy) validateCredentials(ctx context.Context, i *identity.Identity, pw string) error { +func (s *Strategy) validateCredentials(ctx context.Context, i *identity.Identity, pw string) (err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.password.strategy.validateCredentials") + defer otelx.End(span, &err) + if err := s.d.IdentityValidator().Validate(ctx, i); err != nil { return err } diff --git a/selfservice/strategy/password/registration_test.go b/selfservice/strategy/password/registration_test.go index a3c87cbefbf6..fac9ec028fc2 100644 --- a/selfservice/strategy/password/registration_test.go +++ b/selfservice/strategy/password/registration_test.go @@ -14,6 +14,8 @@ import ( "testing" "time" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/driver" "github.com/ory/kratos/internal/registrationhelpers" @@ -45,6 +47,8 @@ func newRegistrationRegistry(t *testing.T) *driver.RegistryDefault { conf, reg := internal.NewFastRegistryWithMocks(t) conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{"enabled": true}) conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationLoginHints, true) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, true) + return reg } @@ -54,6 +58,7 @@ func TestRegistration(t *testing.T) { t.Run("case=registration", func(t *testing.T) { reg := newRegistrationRegistry(t) conf := reg.Config() + conf.MustSet(ctx, "selfservice.flows.registration.enable_legacy_one_step", true) router := x.NewRouterPublic() admin := x.NewRouterAdmin() @@ -103,7 +108,7 @@ func TestRegistration(t *testing.T) { }) }) - var expectLoginBody = func(t *testing.T, browserRedirTS *httptest.Server, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + var expectRegistrationBody = func(t *testing.T, browserRedirTS *httptest.Server, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { if isAPI { return testhelpers.SubmitRegistrationForm(t, isAPI, hc, publicTS, values, isSPA, http.StatusOK, @@ -123,17 +128,17 @@ func TestRegistration(t *testing.T) { isSPA, http.StatusOK, expectReturnTo) } - var expectSuccessfulLogin = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + var expectSuccessfulRegistration = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { useReturnToFromTS(redirTS) - return expectLoginBody(t, redirTS, isAPI, isSPA, hc, values) + return expectRegistrationBody(t, redirTS, isAPI, isSPA, hc, values) } - var expectNoLogin = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { + var expectNoRegistration = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string { useReturnToFromTS(redirNoSessionTS) t.Cleanup(func() { useReturnToFromTS(redirTS) }) - return expectLoginBody(t, redirNoSessionTS, isAPI, isSPA, hc, values) + return expectRegistrationBody(t, redirNoSessionTS, isAPI, isSPA, hc, values) } t.Run("case=should reject invalid transient payload", func(t *testing.T) { @@ -175,7 +180,7 @@ func TestRegistration(t *testing.T) { t.Run("type=api", func(t *testing.T) { username := x.NewUUID().String() - body := expectSuccessfulLogin(t, true, false, nil, func(v url.Values) { + body := expectSuccessfulRegistration(t, true, false, nil, func(v url.Values) { setValues(username, v) }) assert.Equal(t, username, gjson.Get(body, "identity.traits.username").String(), "%s", body) @@ -185,7 +190,7 @@ func TestRegistration(t *testing.T) { t.Run("type=spa", func(t *testing.T) { username := x.NewUUID().String() - body := expectSuccessfulLogin(t, false, true, nil, func(v url.Values) { + body := expectSuccessfulRegistration(t, false, true, nil, func(v url.Values) { setValues(username, v) }) assert.Equal(t, username, gjson.Get(body, "identity.traits.username").String(), "%s", body) @@ -195,7 +200,7 @@ func TestRegistration(t *testing.T) { t.Run("type=browser", func(t *testing.T) { username := x.NewUUID().String() - body := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) { + body := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) { setValues(username, v) }) assert.Equal(t, username, gjson.Get(body, "identity.traits.username").String(), "%s", body) @@ -210,7 +215,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=api", func(t *testing.T) { - body := expectSuccessfulLogin(t, true, false, nil, func(v url.Values) { + body := expectSuccessfulRegistration(t, true, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-api") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -218,10 +223,11 @@ func TestRegistration(t *testing.T) { assert.Equal(t, `registration-identifier-8-api`, gjson.Get(body, "identity.traits.username").String(), "%s", body) assert.NotEmpty(t, gjson.Get(body, "session_token").String(), "%s", body) assert.NotEmpty(t, gjson.Get(body, "session.id").String(), "%s", body) + assert.NotContains(t, gjson.Get(body, "continue_with").Raw, string(flow.ContinueWithActionRedirectBrowserToString), "%s", body) }) t.Run("type=spa", func(t *testing.T) { - body := expectSuccessfulLogin(t, false, true, nil, func(v url.Values) { + body := expectSuccessfulRegistration(t, false, true, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-spa") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -229,15 +235,17 @@ func TestRegistration(t *testing.T) { assert.Equal(t, `registration-identifier-8-spa`, gjson.Get(body, "identity.traits.username").String(), "%s", body) assert.Empty(t, gjson.Get(body, "session_token").String(), "%s", body) assert.NotEmpty(t, gjson.Get(body, "session.id").String(), "%s", body) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) }) t.Run("type=browser", func(t *testing.T) { - body := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) { + body := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-browser") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") }) assert.Equal(t, `registration-identifier-8-browser`, gjson.Get(body, "identity.traits.username").String(), "%s", body) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) }) @@ -246,7 +254,7 @@ func TestRegistration(t *testing.T) { conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil) t.Run("type=api", func(t *testing.T) { - body := expectNoLogin(t, true, false, nil, func(v url.Values) { + body := expectNoRegistration(t, true, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-api-nosession") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -257,7 +265,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=spa", func(t *testing.T) { - expectNoLogin(t, false, true, nil, func(v url.Values) { + expectNoRegistration(t, false, true, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-spa-nosession") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -265,7 +273,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=browser", func(t *testing.T) { - expectNoLogin(t, false, false, nil, func(v url.Values) { + expectNoRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-8-browser-nosession") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -297,7 +305,7 @@ func TestRegistration(t *testing.T) { v.Set("traits.foobar", "bar") } - _ = expectSuccessfulLogin(t, true, false, apiClient, values) + _ = expectSuccessfulRegistration(t, true, false, apiClient, values) body := testhelpers.SubmitRegistrationForm(t, true, apiClient, publicTS, applyTransform(values, transform), false, http.StatusBadRequest, publicTS.URL+registration.RouteSubmitFlow) @@ -311,7 +319,7 @@ func TestRegistration(t *testing.T) { v.Set("traits.foobar", "bar") } - _ = expectSuccessfulLogin(t, false, true, nil, values) + _ = expectSuccessfulRegistration(t, false, true, nil, values) body := registrationhelpers.ExpectValidationError(t, publicTS, conf, "spa", applyTransform(values, transform)) assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "You tried signing in with registration-identifier-8-spa-duplicate-"+suffix+" which is already in use by another account. You can sign in using your password.", "%s", body) }) @@ -323,7 +331,7 @@ func TestRegistration(t *testing.T) { v.Set("traits.foobar", "bar") } - _ = expectSuccessfulLogin(t, false, false, nil, values) + _ = expectSuccessfulRegistration(t, false, false, nil, values) body := registrationhelpers.ExpectValidationError(t, publicTS, conf, "browser", applyTransform(values, transform)) assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "You tried signing in with registration-identifier-8-browser-duplicate-"+suffix+" which is already in use by another account. You can sign in using your password.", "%s", body) }) @@ -538,7 +546,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=api", func(t *testing.T) { - actual := expectSuccessfulLogin(t, true, false, nil, func(v url.Values) { + actual := expectSuccessfulRegistration(t, true, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-10-api") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -547,7 +555,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=spa", func(t *testing.T) { - actual := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) { + actual := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-10-spa") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -556,7 +564,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=browser", func(t *testing.T) { - actual := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) { + actual := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", "registration-identifier-10-browser") v.Set("password", x.NewUUID().String()) v.Set("traits.foobar", "bar") @@ -617,7 +625,7 @@ func TestRegistration(t *testing.T) { username := "registration-custom-schema" t.Run("type=api", func(t *testing.T) { - body := expectNoLogin(t, true, false, nil, func(v url.Values) { + body := expectNoRegistration(t, true, false, nil, func(v url.Values) { v.Set("traits.username", username+"-api") v.Set("password", x.NewUUID().String()) v.Set("traits.baz", "bar") @@ -628,7 +636,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=spa", func(t *testing.T) { - expectNoLogin(t, false, true, nil, func(v url.Values) { + expectNoRegistration(t, false, true, nil, func(v url.Values) { v.Set("traits.username", username+"-spa") v.Set("password", x.NewUUID().String()) v.Set("traits.baz", "bar") @@ -636,7 +644,7 @@ func TestRegistration(t *testing.T) { }) t.Run("type=browser", func(t *testing.T) { - expectNoLogin(t, false, false, nil, func(v url.Values) { + expectNoRegistration(t, false, false, nil, func(v url.Values) { v.Set("traits.username", username+"-browser") v.Set("password", x.NewUUID().String()) v.Set("traits.baz", "bar") @@ -649,6 +657,7 @@ func TestRegistration(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://foo/") + conf.MustSet(ctx, "selfservice.flows.registration.enable_legacy_one_step", true) testhelpers.SetDefaultIdentitySchema(conf, "file://stub/sort.schema.json") conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", true) diff --git a/selfservice/strategy/password/settings.go b/selfservice/strategy/password/settings.go index bc94abcadf1b..3a067ff6d74b 100644 --- a/selfservice/strategy/password/settings.go +++ b/selfservice/strategy/password/settings.go @@ -8,6 +8,10 @@ import ( "net/http" "time" + "golang.org/x/net/context" + + "github.com/ory/x/otelx" + "github.com/ory/kratos/text" "github.com/ory/kratos/ui/node" @@ -56,6 +60,11 @@ type updateSettingsFlowWithPasswordMethod struct { // // swagger:ignore Flow string `json:"flow"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (p *updateSettingsFlowWithPasswordMethod) GetFlowID() uuid.UUID { @@ -66,27 +75,30 @@ func (p *updateSettingsFlowWithPasswordMethod) SetFlowID(rid uuid.UUID) { p.Flow = rid.String() } -func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (*settings.UpdateContext, error) { +func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (_ *settings.UpdateContext, err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.password.strategy.Settings") + defer otelx.End(span, &err) + var p updateSettingsFlowWithPasswordMethod ctxUpdate, err := settings.PrepareUpdate(s.d, w, r, f, ss, settings.ContinuityKey(s.SettingsStrategyID()), &p) if errors.Is(err, settings.ErrContinuePreviousAction) { - return ctxUpdate, s.continueSettingsFlow(w, r, ctxUpdate, &p) + return ctxUpdate, s.continueSettingsFlow(ctx, r, ctxUpdate, p) } else if err != nil { - return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, p, err) } if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.SettingsStrategyID(), s.d); err != nil { - return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, p, err) } if err := s.decodeSettingsFlow(r, &p); err != nil { - return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, p, err) } // This does not come from the payload! p.Flow = ctxUpdate.Flow.ID.String() - if err := s.continueSettingsFlow(w, r, ctxUpdate, &p); err != nil { - return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) + if err := s.continueSettingsFlow(ctx, r, ctxUpdate, p); err != nil { + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, p, err) } return ctxUpdate, nil @@ -99,25 +111,23 @@ func (s *Strategy) decodeSettingsFlow(r *http.Request, dest interface{}) error { } return decoderx.NewHTTP().Decode(r, dest, compiler, + decoderx.HTTPKeepRequestBody(true), decoderx.HTTPDecoderAllowedMethods("POST", "GET"), decoderx.HTTPDecoderSetValidatePayloads(true), decoderx.HTTPDecoderJSONFollowsFormFormat(), ) } -func (s *Strategy) continueSettingsFlow( - w http.ResponseWriter, r *http.Request, - ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithPasswordMethod, -) error { - if err := flow.MethodEnabledAndAllowed(r.Context(), flow.SettingsFlow, s.SettingsStrategyID(), p.Method, s.d); err != nil { +func (s *Strategy) continueSettingsFlow(ctx context.Context, r *http.Request, ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithPasswordMethod) error { + if err := flow.MethodEnabledAndAllowed(ctx, flow.SettingsFlow, s.SettingsStrategyID(), p.Method, s.d); err != nil { return err } - if err := flow.EnsureCSRF(s.d, r, ctxUpdate.Flow.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + if err := flow.EnsureCSRF(s.d, r, ctxUpdate.Flow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { return err } - if ctxUpdate.Session.AuthenticatedAt.Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(r.Context())).Before(time.Now()) { + if ctxUpdate.Session.AuthenticatedAt.Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(ctx)).Before(time.Now()) { return errors.WithStack(settings.NewFlowNeedsReAuth()) } @@ -129,7 +139,7 @@ func (s *Strategy) continueSettingsFlow( go func() { defer close(hpw) defer close(errC) - h, err := s.d.Hasher(r.Context()).Generate(r.Context(), []byte(p.Password)) + h, err := s.d.Hasher(ctx).Generate(ctx, []byte(p.Password)) if err != nil { errC <- err return @@ -137,13 +147,13 @@ func (s *Strategy) continueSettingsFlow( hpw <- h }() - i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), ctxUpdate.Session.Identity.ID) + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ctxUpdate.Session.Identity.ID) if err != nil { return err } i.UpsertCredentialsConfig(s.ID(), []byte("{}"), 0) - if err := s.validateCredentials(r.Context(), i, p.Password); err != nil { + if err := s.validateCredentials(ctx, i, p.Password); err != nil { return err } @@ -170,7 +180,7 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, _ *identity.Identity, return nil } -func (s *Strategy) handleSettingsError(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithPasswordMethod, err error) error { +func (s *Strategy) handleSettingsError(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithPasswordMethod, err error) error { // Do not pause flow if the flow type is an API flow as we can't save cookies in those flows. if e := new(settings.FlowNeedsReAuth); errors.As(err, &e) && ctxUpdate.Flow != nil && ctxUpdate.Flow.Type == flow.TypeBrowser { if err := s.d.ContinuityManager().Pause(r.Context(), w, r, settings.ContinuityKey(s.SettingsStrategyID()), settings.ContinuityOptions(p, ctxUpdate.GetSessionIdentity())...); err != nil { diff --git a/selfservice/strategy/password/settings_test.go b/selfservice/strategy/password/settings_test.go index 3c5a3c9f9615..c670395d5f24 100644 --- a/selfservice/strategy/password/settings_test.go +++ b/selfservice/strategy/password/settings_test.go @@ -13,6 +13,8 @@ import ( "strings" "testing" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/internal/settingshelpers" "github.com/ory/kratos/text" @@ -42,7 +44,8 @@ func init() { func newIdentityWithPassword(email string) *identity.Identity { return &identity.Identity{ - ID: x.NewUUID(), + ID: x.NewUUID(), + NID: x.NewUUID(), Credentials: map[identity.CredentialsType]identity.Credentials{ "password": { Type: "password", @@ -59,6 +62,7 @@ func newIdentityWithPassword(email string) *identity.Identity { func newEmptyIdentity() *identity.Identity { return &identity.Identity{ ID: x.NewUUID(), + NID: x.NewUUID(), State: identity.StateActive, Traits: identity.Traits(`{}`), SchemaID: config.DefaultIdentityTraitsSchemaID, @@ -68,6 +72,7 @@ func newEmptyIdentity() *identity.Identity { func newIdentityWithoutCredentials(email string) *identity.Identity { return &identity.Identity{ ID: x.NewUUID(), + NID: x.NewUUID(), State: identity.StateActive, Traits: identity.Traits(`{"email":"` + email + `"}`), SchemaID: config.DefaultIdentityTraitsSchemaID, @@ -82,7 +87,7 @@ func TestSettings(t *testing.T) { testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true) testhelpers.StrategyEnable(t, conf, settings.StrategyProfile, true) - _ = testhelpers.NewSettingsUIFlowEchoServer(t, reg) + settingsUI := testhelpers.NewSettingsUIFlowEchoServer(t, reg) _ = testhelpers.NewErrorTestServer(t, reg) _ = testhelpers.NewLoginUIWith401Response(t, conf) conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1m") @@ -94,10 +99,10 @@ func TestSettings(t *testing.T) { publicTS, _ := testhelpers.NewKratosServer(t, reg) - browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, browserIdentity1) - browserUser2 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, browserIdentity2) - apiUser1 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, apiIdentity1) - apiUser2 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, apiIdentity2) + browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, browserIdentity1) + browserUser2 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, browserIdentity2) + apiUser1 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, apiIdentity1) + apiUser2 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, apiIdentity2) t.Run("description=not authorized to call endpoints without a session", func(t *testing.T) { c := testhelpers.NewDebugClient(t) @@ -202,8 +207,8 @@ func TestSettings(t *testing.T) { values.Set("method", "password") values.Set("password", x.NewUUID().String()) actual, res := testhelpers.SettingsMakeRequest(t, true, false, f, apiUser2, testhelpers.EncodeFormAsJSON(t, true, values)) - assert.Equal(t, http.StatusBadRequest, res.StatusCode) - assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), "initiated by another person", "%s", actual) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Contains(t, gjson.Get(actual, "error.reason").String(), "initiated by someone else", "%s", actual) }) t.Run("type=spa", func(t *testing.T) { @@ -212,8 +217,8 @@ func TestSettings(t *testing.T) { values.Set("method", "password") values.Set("password", x.NewUUID().String()) actual, res := testhelpers.SettingsMakeRequest(t, false, true, f, browserUser2, values.Encode()) - assert.Equal(t, http.StatusBadRequest, res.StatusCode) - assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), "initiated by another person", "%s", actual) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Contains(t, gjson.Get(actual, "error.reason").String(), "initiated by someone else", "%s", actual) }) t.Run("type=browser", func(t *testing.T) { @@ -223,7 +228,7 @@ func TestSettings(t *testing.T) { values.Set("password", x.NewUUID().String()) actual, res := testhelpers.SettingsMakeRequest(t, false, false, f, browserUser2, values.Encode()) assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), "initiated by another person", "%s", actual) + assert.Contains(t, gjson.Get(actual, "reason").String(), "initiated by someone else", "%s", actual) }) }) @@ -242,15 +247,20 @@ func TestSettings(t *testing.T) { t.Run("type=api", func(t *testing.T) { actual := testhelpers.SubmitSettingsForm(t, true, false, apiUser1, publicTS, payload, http.StatusOK, publicTS.URL+settings.RouteSubmitFlow) check(t, actual) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) }) t.Run("type=spa", func(t *testing.T) { actual := testhelpers.SubmitSettingsForm(t, false, true, browserUser1, publicTS, payload, http.StatusOK, publicTS.URL+settings.RouteSubmitFlow) check(t, actual) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), settingsUI.URL, "%s", actual) }) t.Run("type=browser", func(t *testing.T) { - check(t, testhelpers.SubmitSettingsForm(t, false, false, browserUser1, publicTS, payload, http.StatusOK, conf.SelfServiceFlowSettingsUI(ctx).String())) + actual := testhelpers.SubmitSettingsForm(t, false, false, browserUser1, publicTS, payload, http.StatusOK, conf.SelfServiceFlowSettingsUI(ctx).String()) + check(t, actual) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) }) }) @@ -340,9 +350,9 @@ func TestSettings(t *testing.T) { bi := newIdentityWithoutCredentials(x.NewUUID().String() + "@ory.sh") si := newIdentityWithoutCredentials(x.NewUUID().String() + "@ory.sh") ai := newIdentityWithoutCredentials(x.NewUUID().String() + "@ory.sh") - browserUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, bi) - spaUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, si) - apiUser := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, ai) + browserUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, bi) + spaUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, si) + apiUser := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, ai) var check = func(t *testing.T, actual string, id *identity.Identity) { assert.Equal(t, "success", gjson.Get(actual, "state").String(), "%s", actual) @@ -433,12 +443,12 @@ func TestSettings(t *testing.T) { var initClients = func(isAPI, isSPA bool, id *identity.Identity) (client1, client2 *http.Client) { if isAPI { - client1 = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) - client2 = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + client1 = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) + client2 = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) return client1, client2 } - client1 = testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) - client2 = testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + client1 = testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) + client2 = testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) return client1, client2 } @@ -485,8 +495,8 @@ func TestSettings(t *testing.T) { testhelpers.SetDefaultIdentitySchema(conf, "file://stub/missing-identifier.schema.json") id := newIdentityWithoutCredentials(testhelpers.RandomEmail()) - browser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) - api := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + browser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) + api := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) for _, f := range []string{"spa", "api", "browser"} { t.Run("type="+f, func(t *testing.T) { diff --git a/selfservice/strategy/password/strategy.go b/selfservice/strategy/password/strategy.go index 901412d2574e..a0280ee7e7aa 100644 --- a/selfservice/strategy/password/strategy.go +++ b/selfservice/strategy/password/strategy.go @@ -6,12 +6,14 @@ package password import ( "context" "encoding/json" - - "github.com/ory/kratos/ui/node" + "strings" "github.com/go-playground/validator/v10" "github.com/pkg/errors" + "github.com/ory/kratos/ui/node" + "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/decoderx" "github.com/ory/kratos/continuity" @@ -37,9 +39,10 @@ type registrationStrategyDependencies interface { x.WriterProvider x.CSRFTokenGeneratorProvider x.CSRFProvider - + x.HTTPClientProvider + x.TracingProvider + jsonnetsecure.VMProvider config.Provider - continuity.ManagementProvider errorx.ManagementProvider @@ -65,6 +68,7 @@ type registrationStrategyDependencies interface { identity.PrivilegedPoolProvider identity.ValidationProvider + identity.ManagementProvider session.HandlerProvider session.ManagementProvider @@ -84,7 +88,7 @@ func NewStrategy(d any) *Strategy { } } -func (s *Strategy) CountActiveFirstFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { +func (s *Strategy) CountActiveFirstFactorCredentials(ctx context.Context, cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { for _, c := range cc { if c.Type == s.ID() && len(c.Config) > 0 { var conf identity.CredentialsPassword @@ -92,11 +96,13 @@ func (s *Strategy) CountActiveFirstFactorCredentials(cc map[identity.Credentials return 0, errors.WithStack(err) } - if len(c.Identifiers) > 0 && len(c.Identifiers[0]) > 0 && + if len(strings.Join(c.Identifiers, "")) > 0 && + ((s.d.Config().PasswordMigrationHook(ctx).Enabled && conf.UsePasswordMigrationHook) || + len(conf.HashedPassword) > 0) && (hash.IsBcryptHash([]byte(conf.HashedPassword)) || hash.IsArgon2idHash([]byte(conf.HashedPassword)) || // fandom-start hash.IsFandomLegacyHash([]byte(conf.HashedPassword))) { - // fandom-end + // fandom-end { count++ } } @@ -104,7 +110,7 @@ func (s *Strategy) CountActiveFirstFactorCredentials(cc map[identity.Credentials return } -func (s *Strategy) CountActiveMultiFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { +func (s *Strategy) CountActiveMultiFactorCredentials(_ context.Context, _ map[identity.CredentialsType]identity.Credentials) (count int, err error) { return 0, nil } @@ -112,7 +118,7 @@ func (s *Strategy) ID() identity.CredentialsType { return identity.CredentialsTypePassword } -func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context, _ session.AuthenticationMethods) session.AuthenticationMethod { +func (s *Strategy) CompletedAuthenticationMethod(_ context.Context) session.AuthenticationMethod { return session.AuthenticationMethod{ Method: s.ID(), AAL: identity.AuthenticatorAssuranceLevel1, diff --git a/selfservice/strategy/password/strategy_disabled_test.go b/selfservice/strategy/password/strategy_disabled_test.go index 6cf92147c3a4..e95e98c5913e 100644 --- a/selfservice/strategy/password/strategy_disabled_test.go +++ b/selfservice/strategy/password/strategy_disabled_test.go @@ -4,6 +4,7 @@ package password_test import ( + "context" "io" "net/http" "net/url" @@ -54,7 +55,7 @@ func TestDisabledEndpoint(t *testing.T) { t.Run("case=should not settings when password method is disabled", func(t *testing.T) { testhelpers.SetDefaultIdentitySchema(conf, "file://stub/login.schema.json") - c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg) + c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, context.Background(), reg) t.Run("method=GET", func(t *testing.T) { t.Skip("GET is currently not supported for this endpoint.") diff --git a/selfservice/strategy/password/strategy_test.go b/selfservice/strategy/password/strategy_test.go index e27a0950ca28..dc86e5f1a85e 100644 --- a/selfservice/strategy/password/strategy_test.go +++ b/selfservice/strategy/password/strategy_test.go @@ -4,20 +4,36 @@ package password_test import ( + "cmp" "context" + "encoding/json" "fmt" "testing" + "github.com/ory/kratos/driver/config" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + hash2 "github.com/ory/kratos/hash" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/go-faker/faker/v4" + "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" "github.com/ory/kratos/selfservice/strategy/password" ) +func generateRandomConfig(t *testing.T) (identity.CredentialsPassword, []byte) { + t.Helper() + var cred identity.CredentialsPassword + require.NoError(t, faker.FakeData(&cred)) + c, err := json.Marshal(cred) + require.NoError(t, err) + return cred, c +} + func TestCountActiveFirstFactorCredentials(t *testing.T) { ctx := context.Background() _, reg := internal.NewFastRegistryWithMocks(t) @@ -28,75 +44,127 @@ func TestCountActiveFirstFactorCredentials(t *testing.T) { h2, err := reg.Hasher(ctx).Generate(context.Background(), []byte("a password")) require.NoError(t, err) - for k, tc := range []struct { - in map[identity.CredentialsType]identity.Credentials - expected int - }{ - { - in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { - Type: strategy.ID(), - Config: []byte{}, - }}, - expected: 0, - }, - { - in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { - Type: strategy.ID(), - Config: []byte(`{"hashed_password": "` + string(h1) + `"}`), - }}, - expected: 0, - }, - { - in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { - Type: strategy.ID(), - Identifiers: []string{""}, - Config: []byte(`{"hashed_password": "` + string(h1) + `"}`), - }}, - expected: 0, - }, - { - in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { - Type: strategy.ID(), - Identifiers: []string{"foo"}, - Config: []byte(`{"hashed_password": "` + string(h1) + `"}`), - }}, - expected: 1, - }, - { - in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { - Type: strategy.ID(), - Identifiers: []string{"foo"}, - Config: []byte(`{"hashed_password": "` + string(h2) + `"}`), - }}, - expected: 1, - }, - { - in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { - Type: strategy.ID(), - Config: []byte(`{"hashed_password": "asdf"}`), - }}, - expected: 0, - }, - { - in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { - Type: strategy.ID(), - Config: []byte(`{}`), - }}, - expected: 0, - }, - { - in: nil, - expected: 0, - }, - } { - t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { - actual, err := strategy.CountActiveFirstFactorCredentials(tc.in) - assert.NoError(t, err) - assert.Equal(t, tc.expected, actual) + t.Run("test regressions fixtures", func(t *testing.T) { + // This test ensures we do not add regressions to this method by, for example, adding a new field. + for k := 0; k < 100; k++ { + t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) { + cred, c := generateRandomConfig(t) + actual, err := strategy.CountActiveFirstFactorCredentials(ctx, map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Identifiers: []string{"foo"}, + Config: c, + }}) + assert.NoError(t, err) + + if len(cred.HashedPassword) == 0 && cred.UsePasswordMigrationHook { + // This case is OK + assert.Equal(t, 0, actual) + return + } + + assert.Equal(t, 1, actual) + }) + } + }) + + t.Run("with fixtures", func(t *testing.T) { + for k, tc := range []struct { + in map[identity.CredentialsType]identity.Credentials + expected int + ctx context.Context + }{ + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte{}, + }}, + expected: 0, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte(`{"hashed_password": "` + string(h1) + `"}`), + }}, + expected: 0, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Identifiers: []string{""}, + Config: []byte(`{"hashed_password": "` + string(h1) + `"}`), + }}, + expected: 0, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Identifiers: []string{"foo"}, + Config: []byte(`{"hashed_password": "` + string(h1) + `"}`), + }}, + expected: 1, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Identifiers: []string{"foo"}, + Config: []byte(`{"hashed_password": "` + string(h2) + `"}`), + }}, + expected: 1, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Identifiers: []string{"foo"}, + Config: []byte(`{"use_password_migration_hook":true}`), + }}, + expected: 1, + ctx: confighelpers.WithConfigValue(ctx, config.ViperKeyPasswordMigrationHook+".enabled", true), + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Identifiers: []string{"foo"}, + Config: []byte(`{"use_password_migration_hook":true}`), + }}, + expected: 0, + ctx: confighelpers.WithConfigValue(ctx, config.ViperKeyPasswordMigrationHook+".enabled", false), + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Identifiers: []string{"foo"}, + Config: []byte(`{"use_password_migration_hook":false}`), + }}, + expected: 0, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte(`{"hashed_password": "asdf"}`), + }}, + expected: 0, + }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte(`{}`), + }}, + expected: 0, + }, + { + in: nil, + expected: 0, + }, + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + actual, err := strategy.CountActiveFirstFactorCredentials(cmp.Or(tc.ctx, ctx), tc.in) + assert.NoError(t, err) + assert.Equal(t, tc.expected, actual) - actual, err = strategy.CountActiveMultiFactorCredentials(tc.in) - assert.NoError(t, err) - assert.Equal(t, 0, actual) - }) - } + actual, err = strategy.CountActiveMultiFactorCredentials(cmp.Or(tc.ctx, ctx), tc.in) + assert.NoError(t, err) + assert.Equal(t, 0, actual) + }) + } + }) } diff --git a/selfservice/strategy/password/stub/migration.schema.json b/selfservice/strategy/password/stub/migration.schema.json new file mode 100644 index 000000000000..5ac7314cd7d9 --- /dev/null +++ b/selfservice/strategy/password/stub/migration.schema.json @@ -0,0 +1,36 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + } + }, + "required": [ + "email" + ] + } + }, + "additionalProperties": false +} diff --git a/selfservice/strategy/password/types.go b/selfservice/strategy/password/types.go index 61a4f73cb4ae..6a3515c1a3dc 100644 --- a/selfservice/strategy/password/types.go +++ b/selfservice/strategy/password/types.go @@ -3,9 +3,7 @@ package password -import ( - "github.com/ory/kratos/ui/container" -) +import "encoding/json" // Update Login Flow with Password Method // @@ -32,9 +30,9 @@ type updateLoginFlowWithPasswordMethod struct { // // required: true Identifier string `json:"identifier"` -} -// FlowMethod contains the configuration for this selfservice strategy. -type FlowMethod struct { - *container.Container + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } diff --git a/selfservice/strategy/profile/.schema/registration.schema.json b/selfservice/strategy/profile/.schema/registration.schema.json new file mode 100644 index 000000000000..727b8f86c7d5 --- /dev/null +++ b/selfservice/strategy/profile/.schema/registration.schema.json @@ -0,0 +1,27 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/profile/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "csrf_token": { + "type": "string" + }, + "traits": { + "description": "This field will be overwritten in registration.go's decoder() method. Do not add anything to this field as it has no effect." + }, + "screen": { + "type": "string", + "enum": [ + "credential-selection", + "previous" + ] + }, + "method": { + "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true + } + } +} diff --git a/selfservice/strategy/profile/.schema/settings.schema.json b/selfservice/strategy/profile/.schema/settings.schema.json index 4241cbdc22bb..90246cbde523 100644 --- a/selfservice/strategy/profile/.schema/settings.schema.json +++ b/selfservice/strategy/profile/.schema/settings.schema.json @@ -12,6 +12,13 @@ "traits": {}, "csrf_token": { "type": "string" + }, + "action": { + "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=api#01.json b/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=spa.json similarity index 100% rename from selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=api#01.json rename to selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=spa.json diff --git a/selfservice/strategy/profile/strategy.go b/selfservice/strategy/profile/strategy.go index 5b8d7f368306..996a2b31f5e1 100644 --- a/selfservice/strategy/profile/strategy.go +++ b/selfservice/strategy/profile/strategy.go @@ -9,8 +9,10 @@ import ( "net/http" "time" - "github.com/ory/jsonschema/v3" + "github.com/ory/x/otelx" + "github.com/ory/jsonschema/v3" + "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/text" "github.com/gofrs/uuid" @@ -40,6 +42,7 @@ type ( x.CSRFTokenGeneratorProvider x.WriterProvider x.LoggingProvider + x.TracingProvider config.Provider @@ -60,7 +63,9 @@ type ( settings.StrategyProvider settings.HooksProvider - schema.IdentityTraitsProvider + registration.FlowPersistenceProvider + + schema.IdentitySchemaProvider } Strategy struct { d strategyDependencies @@ -76,22 +81,22 @@ func (s *Strategy) SettingsStrategyID() string { return settings.StrategyProfile } -func (s *Strategy) RegisterSettingsRoutes(public *x.RouterPublic) {} +func (s *Strategy) RegisterSettingsRoutes(_ *x.RouterPublic) {} func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity, f *settings.Flow) error { - schemas, err := s.d.Config().IdentityTraitsSchemas(r.Context()) + schemas, err := s.d.IdentityTraitsSchemas(r.Context()) if err != nil { return err } - traitsSchema, err := schemas.FindSchemaByID(id.SchemaID) + traitsSchema, err := schemas.GetByID(id.SchemaID) if err != nil { return err } // use a schema compiler that disables identifiers schemaCompiler := jsonschema.NewCompiler() - nodes, err := container.NodesFromJSONSchema(r.Context(), node.ProfileGroup, traitsSchema.URL, "", schemaCompiler) + nodes, err := container.NodesFromJSONSchema(r.Context(), node.ProfileGroup, traitsSchema.URL.String(), "", schemaCompiler) if err != nil { return err } @@ -107,22 +112,25 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity return nil } -func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (*settings.UpdateContext, error) { +func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (_ *settings.UpdateContext, err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.profile.strategy.Settings") + defer otelx.End(span, &err) + var p updateSettingsFlowWithProfileMethod ctxUpdate, err := settings.PrepareUpdate(s.d, w, r, f, ss, settings.ContinuityKey(s.SettingsStrategyID()), &p) if errors.Is(err, settings.ErrContinuePreviousAction) { - return ctxUpdate, s.continueFlow(w, r, ctxUpdate, &p) + return ctxUpdate, s.continueFlow(ctx, r, ctxUpdate, p) } else if err != nil { - return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, nil, &p, err) + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, nil, p, err) } if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.SettingsStrategyID(), s.d); err != nil { return ctxUpdate, err } - option, err := s.newSettingsProfileDecoder(r.Context(), ctxUpdate.GetSessionIdentity()) + option, err := s.newSettingsProfileDecoder(ctx, ctxUpdate.GetSessionIdentity()) if err != nil { - return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, nil, &p, err) + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, nil, p, err) } if err := s.dc.Decode(r, &p, option, @@ -130,25 +138,25 @@ func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings. decoderx.HTTPDecoderSetValidatePayloads(true), decoderx.HTTPDecoderJSONFollowsFormFormat(), ); err != nil { - return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, nil, &p, err) + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, nil, p, err) } // Reset after decoding form p.SetFlowID(ctxUpdate.Flow.ID) - if err := s.continueFlow(w, r, ctxUpdate, &p); err != nil { - return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, nil, &p, err) + if err := s.continueFlow(ctx, r, ctxUpdate, p); err != nil { + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, nil, p, err) } return ctxUpdate, nil } -func (s *Strategy) continueFlow(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithProfileMethod) error { - if err := flow.MethodEnabledAndAllowed(r.Context(), flow.SettingsFlow, s.SettingsStrategyID(), p.Method, s.d); err != nil { +func (s *Strategy) continueFlow(ctx context.Context, r *http.Request, ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithProfileMethod) error { + if err := flow.MethodEnabledAndAllowed(ctx, flow.SettingsFlow, s.SettingsStrategyID(), p.Method, s.d); err != nil { return err } - if err := flow.EnsureCSRF(s.d, r, ctxUpdate.Flow.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + if err := flow.EnsureCSRF(s.d, r, ctxUpdate.Flow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { return err } @@ -156,17 +164,17 @@ func (s *Strategy) continueFlow(w http.ResponseWriter, r *http.Request, ctxUpdat return errors.WithStack(herodot.ErrBadRequest.WithReasonf("Did not receive any value changes.")) } - if err := s.hydrateForm(r, ctxUpdate.Flow, ctxUpdate.Session, p.Traits); err != nil { + if err := s.hydrateForm(r, ctxUpdate.Flow, p.Traits); err != nil { return err } options := []identity.ManagerOption{identity.ManagerExposeValidationErrorsForInternalTypeAssertion} - ttl := s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(r.Context()) + ttl := s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(ctx) if ctxUpdate.Session.AuthenticatedAt.Add(ttl).After(time.Now()) { options = append(options, identity.ManagerAllowWriteProtectedTraits) } - update, err := s.d.IdentityManager().SetTraits(r.Context(), ctxUpdate.GetSessionIdentity().ID, identity.Traits(p.Traits), options...) + update, err := s.d.IdentityManager().SetTraits(ctx, ctxUpdate.GetSessionIdentity().ID, identity.Traits(p.Traits), options...) if err != nil { if errors.Is(err, identity.ErrProtectedFieldModified) { return settings.NewFlowNeedsReAuth() @@ -208,6 +216,11 @@ type updateSettingsFlowWithProfileMethod struct { // // This token is only required when performing browser flows. CSRFToken string `json:"csrf_token"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (p *updateSettingsFlowWithProfileMethod) GetFlowID() uuid.UUID { @@ -218,7 +231,7 @@ func (p *updateSettingsFlowWithProfileMethod) SetFlowID(rid uuid.UUID) { p.FlowID = rid.String() } -func (s *Strategy) hydrateForm(r *http.Request, ar *settings.Flow, ss *session.Session, traits json.RawMessage) error { +func (s *Strategy) hydrateForm(r *http.Request, ar *settings.Flow, traits json.RawMessage) error { if traits != nil { ar.UI.Nodes.ResetNodesWithPrefix("traits.") ar.UI.UpdateNodeValuesFromJSON(traits, "traits", node.ProfileGroup) @@ -230,7 +243,7 @@ func (s *Strategy) hydrateForm(r *http.Request, ar *settings.Flow, ss *session.S // handleSettingsError is a convenience function for handling all types of errors that may occur (e.g. validation error) // during a settings request. -func (s *Strategy) handleSettingsError(w http.ResponseWriter, r *http.Request, puc *settings.UpdateContext, traits json.RawMessage, p *updateSettingsFlowWithProfileMethod, err error) error { +func (s *Strategy) handleSettingsError(w http.ResponseWriter, r *http.Request, puc *settings.UpdateContext, traits json.RawMessage, p updateSettingsFlowWithProfileMethod, err error) error { if e := new(settings.FlowNeedsReAuth); errors.As(err, &e) { if err := s.d.ContinuityManager().Pause(r.Context(), w, r, settings.ContinuityKey(s.SettingsStrategyID()), @@ -248,7 +261,7 @@ func (s *Strategy) handleSettingsError(w http.ResponseWriter, r *http.Request, p } } - if err := s.hydrateForm(r, puc.Flow, puc.Session, traits); err != nil { + if err := s.hydrateForm(r, puc.Flow, traits); err != nil { return err } } diff --git a/selfservice/strategy/profile/strategy_test.go b/selfservice/strategy/profile/strategy_test.go index f67407fe799f..d34c3f9e94f6 100644 --- a/selfservice/strategy/profile/strategy_test.go +++ b/selfservice/strategy/profile/strategy_test.go @@ -89,13 +89,19 @@ func TestStrategyTraits(t *testing.T) { browserIdentity1 := newIdentityWithPassword("john-browser@doe.com") apiIdentity1 := newIdentityWithPassword("john-api@doe.com") - browserIdentity2 := &identity.Identity{ID: x.NewUUID(), Traits: identity.Traits(`{}`), State: identity.StateActive} - apiIdentity2 := &identity.Identity{ID: x.NewUUID(), Traits: identity.Traits(`{}`), State: identity.StateActive} - - browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, browserIdentity1) - browserUser2 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, browserIdentity2) - apiUser1 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, apiIdentity1) - apiUser2 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, apiIdentity2) + browserID2 := x.NewUUID() + browserIdentity2 := &identity.Identity{ID: browserID2, Traits: identity.Traits(`{}`), State: identity.StateActive, Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Type: "password", Identifiers: []string{browserID2.String()}, Config: []byte(`{"hashed_password":"$2a$04$zvZz1zV"}`)}, + }} + apiID2 := x.NewUUID() + apiIdentity2 := &identity.Identity{ID: apiID2, Traits: identity.Traits(`{}`), State: identity.StateActive, Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Type: "password", Identifiers: []string{apiID2.String()}, Config: []byte(`{"hashed_password":"$2a$04$zvZz1zV"}`)}, + }} + + browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, browserIdentity1) + browserUser2 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, browserIdentity2) + apiUser1 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, apiIdentity1) + apiUser2 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, apiIdentity2) t.Run("description=not authorized to call endpoints without a session", func(t *testing.T) { setUnprivileged(t) @@ -205,13 +211,13 @@ func TestStrategyTraits(t *testing.T) { } t.Run("type=api", func(t *testing.T) { - pr, _, err := testhelpers.NewSDKCustomClient(publicTS, apiUser1).FrontendApi.CreateNativeSettingsFlow(context.Background()).Execute() + pr, _, err := testhelpers.NewSDKCustomClient(publicTS, apiUser1).FrontendAPI.CreateNativeSettingsFlow(context.Background()).Execute() require.NoError(t, err) run(t, apiIdentity1, pr, settings.RouteInitAPIFlow) }) - t.Run("type=api", func(t *testing.T) { - pr, _, err := testhelpers.NewSDKCustomClient(publicTS, browserUser1).FrontendApi.CreateBrowserSettingsFlow(context.Background()).Execute() + t.Run("type=spa", func(t *testing.T) { + pr, _, err := testhelpers.NewSDKCustomClient(publicTS, browserUser1).FrontendAPI.CreateBrowserSettingsFlow(context.Background()).Execute() require.NoError(t, err) run(t, browserIdentity1, pr, settings.RouteInitBrowserFlow) }) @@ -224,7 +230,7 @@ func TestStrategyTraits(t *testing.T) { rid := res.Request.URL.Query().Get("flow") require.NotEmpty(t, rid) - pr, _, err := testhelpers.NewSDKCustomClient(publicTS, browserUser1).FrontendApi.GetSettingsFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute() + pr, _, err := testhelpers.NewSDKCustomClient(publicTS, browserUser1).FrontendAPI.GetSettingsFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute() require.NoError(t, err, "%s", rid) run(t, browserIdentity1, pr, settings.RouteInitBrowserFlow) @@ -275,8 +281,8 @@ func TestStrategyTraits(t *testing.T) { values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) actual, res := testhelpers.SettingsMakeRequest(t, true, false, f, apiUser2, testhelpers.EncodeFormAsJSON(t, true, values)) - assert.Equal(t, http.StatusBadRequest, res.StatusCode) - assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), "initiated by another person", "%s", actual) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Contains(t, gjson.Get(actual, "error.reason").String(), "initiated by someone else", "%s", actual) }) t.Run("type=spa", func(t *testing.T) { @@ -284,8 +290,8 @@ func TestStrategyTraits(t *testing.T) { values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) actual, res := testhelpers.SettingsMakeRequest(t, false, true, f, browserUser2, testhelpers.EncodeFormAsJSON(t, true, values)) - assert.Equal(t, http.StatusBadRequest, res.StatusCode) - assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), "initiated by another person", "%s", actual) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + assert.Contains(t, gjson.Get(actual, "error.reason").String(), "initiated by someone else", "%s", actual) }) t.Run("type=browser", func(t *testing.T) { @@ -294,7 +300,7 @@ func TestStrategyTraits(t *testing.T) { values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) actual, res := testhelpers.SettingsMakeRequest(t, false, false, f, browserUser2, values.Encode()) assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), "initiated by another person", "%s", actual) + assert.Contains(t, gjson.Get(actual, "reason").String(), "initiated by someone else", "%s", actual) }) }) @@ -449,15 +455,20 @@ func TestStrategyTraits(t *testing.T) { t.Run("type=api", func(t *testing.T) { actual := expectSuccess(t, true, false, apiUser1, payload("not-john-doe-api@mail.com")) check(t, actual) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) }) t.Run("type=sqa", func(t *testing.T) { actual := expectSuccess(t, false, true, browserUser1, payload("not-john-doe-browser@mail.com")) check(t, actual) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), ui.URL, "%s", actual) }) t.Run("type=browser", func(t *testing.T) { - check(t, expectSuccess(t, false, false, browserUser1, payload("not-john-doe-browser@mail.com"))) + actual := expectSuccess(t, false, false, browserUser1, payload("not-john-doe-browser@mail.com")) + check(t, actual) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) }) }) @@ -612,7 +623,7 @@ func TestDisabledEndpoint(t *testing.T) { publicTS, _ := testhelpers.NewKratosServer(t, reg) browserIdentity1 := newIdentityWithPassword("john-browser@doe.com") - browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, browserIdentity1) + browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, context.Background(), reg, browserIdentity1) t.Run("case=should not submit when profile method is disabled", func(t *testing.T) { t.Run("method=GET", func(t *testing.T) { diff --git a/selfservice/strategy/profile/two_step_registration.go b/selfservice/strategy/profile/two_step_registration.go new file mode 100644 index 000000000000..d6f037a54a92 --- /dev/null +++ b/selfservice/strategy/profile/two_step_registration.go @@ -0,0 +1,256 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package profile + +import ( + "context" + _ "embed" + "encoding/json" + "net/http" + + "github.com/ory/x/otelx/semconv" + + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/otelx" + + "github.com/tidwall/gjson" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/container" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" +) + +//go:embed .schema/registration.schema.json +var registrationSchema []byte + +var _ registration.Strategy = new(Strategy) + +func (s *Strategy) ID() identity.CredentialsType { + return identity.CredentialsTypeProfile +} + +func (s *Strategy) RegisterRegistrationRoutes(*x.RouterPublic) {} + +func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.Flow) error { + if !s.d.Config().SelfServiceFlowRegistrationTwoSteps(r.Context()) { + return nil + } + + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + + nodes, err := container.NodesFromJSONSchema(r.Context(), node.DefaultGroup, ds.String(), "", nil) + if err != nil { + return err + } + + for _, n := range nodes { + f.UI.SetNode(n) + } + + f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + f.UI.Nodes.Append(node.NewInputField( + "method", + "profile", + node.ProfileGroup, + node.InputAttributeTypeSubmit, + ).WithMetaLabel(text.NewInfoRegistration())) + + return nil +} + +// The RegistrationScreen +// swagger:enum RegistrationScreen +type RegistrationScreen string + +const ( + //nolint:gosec // not a credential + RegistrationScreenCredentialSelection RegistrationScreen = "credential-selection" + RegistrationScreenPrevious RegistrationScreen = "previous" +) + +// Update Registration Flow with Profile Method +// +// swagger:model updateRegistrationFlowWithProfileMethod +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type updateRegistrationFlowWithProfileMethod struct { + // Traits + // + // The identity's traits. + // + // required: true + Traits json.RawMessage `json:"traits"` + + // Method + // + // Should be set to profile when trying to update a profile. + // + // required: true + Method string `json:"method"` + + // Screen requests navigation to a previous screen. + // + // This must be set to credential-selection to go back to the credential + // selection screen. + // + // required: false + Screen RegistrationScreen `json:"screen" form:"screen"` + + // FlowIDRequestID is the flow ID. + // + // swagger:ignore + FlowID string `json:"flow"` + + // The Anti-CSRF Token + // + // This token is only required when performing browser flows. + CSRFToken string `json:"csrf_token"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty"` +} + +func (s *Strategy) decode(p *updateRegistrationFlowWithProfileMethod, r *http.Request) error { + return registration.DecodeBody(p, r, s.dc, s.d.Config(), registrationSchema) +} + +func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, i *identity.Identity) (err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.profile.Strategy.Register") + defer otelx.End(span, &err) + + if !s.d.Config().SelfServiceFlowRegistrationTwoSteps(ctx) { + span.SetAttributes(attribute.String("not_responsible_reason", "two-step registration is not enabled")) + return flow.ErrStrategyNotResponsible + } + + var params updateRegistrationFlowWithProfileMethod + + if err = s.decode(¶ms, r); err != nil { + return s.handleRegistrationError(r, regFlow, params, err) + } + + if params.Method == "profile" || params.Screen == RegistrationScreenCredentialSelection { + return s.displayStepTwoNodes(ctx, w, r, regFlow, i, params) + } else if params.Method == "profile:back" { + // "profile:back" is kept for backwards compatibility. + span.AddEvent(semconv.NewDeprecatedFeatureUsedEvent(ctx, "profile:back")) + return s.displayStepOneNodes(ctx, w, r, regFlow, params) + } else if params.Screen == RegistrationScreenPrevious { + return s.displayStepOneNodes(ctx, w, r, regFlow, params) + } + + // Default case + span.SetAttributes(attribute.String("not_responsible_reason", "method mismatch")) + return flow.ErrStrategyNotResponsible +} + +func (s *Strategy) displayStepOneNodes(ctx context.Context, w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, params updateRegistrationFlowWithProfileMethod) error { + regFlow.UI.ResetMessages() + err := json.Unmarshal([]byte(gjson.GetBytes(regFlow.InternalContext, "stepOneNodes").Raw), ®Flow.UI.Nodes) + if err != nil { + return s.handleRegistrationError(r, regFlow, params, err) + } + regFlow.UI.UpdateNodeValuesFromJSON(params.Traits, "traits", node.DefaultGroup) + + if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, regFlow); err != nil { + return s.handleRegistrationError(r, regFlow, params, err) + } + + redirectTo := regFlow.AppendTo(s.d.Config().SelfServiceFlowRegistrationUI(ctx)).String() + if x.IsJSONRequest(r) { + s.d.Writer().WriteCode(w, r, http.StatusBadRequest, regFlow) + } else { + http.Redirect(w, r, redirectTo, http.StatusSeeOther) + } + + return flow.ErrCompletedByStrategy +} + +func (s *Strategy) displayStepTwoNodes(ctx context.Context, w http.ResponseWriter, r *http.Request, regFlow *registration.Flow, i *identity.Identity, params updateRegistrationFlowWithProfileMethod) error { + // Reset state-esque flow fields + regFlow.Active = "" + regFlow.State = "choose_method" + + regFlow.UI.ResetMessages() + regFlow.TransientPayload = params.TransientPayload + + if err := flow.EnsureCSRF(s.d, r, regFlow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, params.CSRFToken); err != nil { + return s.handleRegistrationError(r, regFlow, params, err) + } + + if len(params.Traits) == 0 { + params.Traits = json.RawMessage("{}") + } + i.Traits = identity.Traits(params.Traits) + if err := s.d.IdentityValidator().Validate(ctx, i); err != nil { + return s.handleRegistrationError(r, regFlow, params, err) + } + + err := json.Unmarshal([]byte(gjson.GetBytes(regFlow.InternalContext, "stepTwoNodes").Raw), ®Flow.UI.Nodes) + if err != nil { + return s.handleRegistrationError(r, regFlow, params, err) + } + + regFlow.UI.Messages.Add(text.NewInfoSelfServiceChooseCredentials()) + + regFlow.UI.Nodes.Append(node.NewInputField( + "screen", + "previous", + node.ProfileGroup, + node.InputAttributeTypeSubmit, + ).WithMetaLabel(text.NewInfoRegistrationBack())) + + regFlow.UI.UpdateNodeValuesFromJSON(json.RawMessage(i.Traits), "traits", node.DefaultGroup) + for _, n := range regFlow.UI.Nodes { + if n.Group != node.DefaultGroup || n.Type != node.Input { + continue + } + if attr, ok := n.Attributes.(*node.InputAttributes); ok { + attr.Type = node.InputAttributeTypeHidden + } + } + + if regFlow.Type == flow.TypeBrowser { + regFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + + if err = s.d.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, regFlow); err != nil { + return s.handleRegistrationError(r, regFlow, params, err) + } + + redirectTo := regFlow.AppendTo(s.d.Config().SelfServiceFlowRegistrationUI(ctx)).String() + if x.IsJSONRequest(r) { + s.d.Writer().WriteCode(w, r, http.StatusBadRequest, regFlow) + } else { + http.Redirect(w, r, redirectTo, http.StatusSeeOther) + } + + return flow.ErrCompletedByStrategy +} + +func (s *Strategy) handleRegistrationError(r *http.Request, regFlow *registration.Flow, params updateRegistrationFlowWithProfileMethod, err error) error { + if regFlow != nil { + for _, n := range container.NewFromJSON("", node.ProfileGroup, params.Traits, "traits").Nodes { + // we only set the value and not the whole field because we want to keep types from the initial form generation + regFlow.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue()) + } + + if regFlow.Type == flow.TypeBrowser { + regFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + } + } + + return err +} diff --git a/selfservice/strategy/totp/.schema/login.schema.json b/selfservice/strategy/totp/.schema/login.schema.json index 2ef6aeaf9a9d..fb32f6a08f1f 100644 --- a/selfservice/strategy/totp/.schema/login.schema.json +++ b/selfservice/strategy/totp/.schema/login.schema.json @@ -16,6 +16,10 @@ "totp_code": { "type": "string", "minLength": 6 + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } } } diff --git a/selfservice/strategy/totp/.schema/settings.schema.json b/selfservice/strategy/totp/.schema/settings.schema.json index cab5d763b33a..8767f5a08f41 100644 --- a/selfservice/strategy/totp/.schema/settings.schema.json +++ b/selfservice/strategy/totp/.schema/settings.schema.json @@ -14,6 +14,10 @@ }, "totp_unlink": { "type": "boolean" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } }, "if": { diff --git a/selfservice/strategy/totp/generator.go b/selfservice/strategy/totp/generator.go index fe79d8991d0d..9846506f1671 100644 --- a/selfservice/strategy/totp/generator.go +++ b/selfservice/strategy/totp/generator.go @@ -25,6 +25,7 @@ import ( // So we need 160/8 = 20 key length. stdtotp.Generate uses the key // length for reading from crypto.Rand. const secretSize = 160 / 8 +const digits = otp.DigitsSix func NewKey(ctx context.Context, accountName string, d interface { config.Provider @@ -33,7 +34,7 @@ func NewKey(ctx context.Context, accountName string, d interface { Issuer: d.Config().TOTPIssuer(ctx), AccountName: accountName, SecretSize: secretSize, - Digits: otp.DigitsSix, + Digits: digits, Period: 30, }) if err != nil { diff --git a/selfservice/strategy/totp/login.go b/selfservice/strategy/totp/login.go index 1eaa5aeedac7..d17bc1fd7e86 100644 --- a/selfservice/strategy/totp/login.go +++ b/selfservice/strategy/totp/login.go @@ -7,6 +7,10 @@ import ( "encoding/json" "net/http" + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/otelx" + "github.com/pkg/errors" "github.com/pquerna/otp" "github.com/pquerna/otp/totp" @@ -83,10 +87,19 @@ type updateLoginFlowWithTotpMethod struct { // // required: true TOTPCode string `json:"totp_code"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } -func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, sess *session.Session) (i *identity.Identity, err error) { +func (s *Strategy) Login(_ http.ResponseWriter, r *http.Request, f *login.Flow, sess *session.Session) (i *identity.Identity, err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.totp.strategy.Login") + defer otelx.End(span, &err) + if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel2); err != nil { + span.SetAttributes(attribute.String("not_responsible_reason", "requested AAL is not AAL2")) return nil, err } @@ -101,12 +114,13 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { return nil, s.handleLoginError(r, f, err) } + f.TransientPayload = p.TransientPayload - if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { return nil, s.handleLoginError(r, f, err) } - i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), s.ID(), sess.IdentityID.String()) + i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, s.ID(), sess.IdentityID.String()) if err != nil { return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewNoTOTPDeviceRegistered())) } @@ -126,7 +140,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, } f.Active = s.ID() - if err = s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), f); err != nil { + if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error()))) } diff --git a/selfservice/strategy/totp/login_test.go b/selfservice/strategy/totp/login_test.go index 6456ea7cc599..7a48424ab412 100644 --- a/selfservice/strategy/totp/login_test.go +++ b/selfservice/strategy/totp/login_test.go @@ -13,6 +13,8 @@ import ( "testing" "time" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/x/assertx" "github.com/gofrs/uuid" @@ -107,7 +109,7 @@ func TestCompleteLogin(t *testing.T) { t.Run("case=totp payload is set when identity has totp", func(t *testing.T) { id, _, _ := createIdentity(t, reg) - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ "0.attributes.value", @@ -117,7 +119,7 @@ func TestCompleteLogin(t *testing.T) { t.Run("case=totp payload is not set when identity has no totp", func(t *testing.T) { id := createIdentityWithoutTOTP(t, reg) - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) assertx.EqualAsJSON(t, nil, f.Ui.Nodes) }) @@ -126,7 +128,7 @@ func TestCompleteLogin(t *testing.T) { id, _, _ := createIdentity(t, reg) t.Run("type=api", func(t *testing.T) { - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) body, res := testhelpers.LoginMakeRequest(t, true, false, f, apiClient, "14=)=!(%)$/ZP()GHIÖ") @@ -136,7 +138,7 @@ func TestCompleteLogin(t *testing.T) { }) t.Run("type=browser", func(t *testing.T) { - browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) body, res := testhelpers.LoginMakeRequest(t, false, false, f, browserClient, "14=)=!(%)$/ZP()GHIÖ") @@ -146,7 +148,7 @@ func TestCompleteLogin(t *testing.T) { }) t.Run("type=spa", func(t *testing.T) { - browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, true, false, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) body, res := testhelpers.LoginMakeRequest(t, false, true, f, browserClient, "14=)=!(%)$/ZP()GHIÖ") @@ -157,7 +159,7 @@ func TestCompleteLogin(t *testing.T) { }) doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) { - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) values.Set("method", "totp") @@ -167,7 +169,7 @@ func TestCompleteLogin(t *testing.T) { } doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity, returnTo string) (string, *http.Response) { - browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) opts := []testhelpers.InitFlowWithOption{testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)} if len(returnTo) > 0 { @@ -333,23 +335,36 @@ func TestCompleteLogin(t *testing.T) { t.Run("type=api", func(t *testing.T) { body, res := doAPIFlow(t, payload, id) check(t, false, body, res) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) t.Run("type=browser", func(t *testing.T) { body, res := doBrowserFlow(t, false, payload, id, "") check(t, true, body, res) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) t.Run("type=browser set return_to", func(t *testing.T) { returnTo := "https://www.ory.sh" - _, res := doBrowserFlow(t, false, payload, id, returnTo) + body, res := doBrowserFlow(t, false, payload, id, returnTo) t.Log(res.Request.URL.String()) assert.Contains(t, res.Request.URL.String(), returnTo) + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) }) t.Run("type=spa", func(t *testing.T) { body, res := doBrowserFlow(t, true, payload, id, "") check(t, false, body, res) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.EqualValues(t, conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), gjson.Get(body, "continue_with.0.redirect_browser_to").String(), "%s", body) + }) + + t.Run("type=spa set return_to", func(t *testing.T) { + returnTo := "https://www.ory.sh" + body, res := doBrowserFlow(t, true, payload, id, returnTo) + check(t, false, body, res) + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.EqualValues(t, returnTo, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), "%s", body) }) }) diff --git a/selfservice/strategy/totp/settings.go b/selfservice/strategy/totp/settings.go index 0587e87e2d6a..be0cce18afeb 100644 --- a/selfservice/strategy/totp/settings.go +++ b/selfservice/strategy/totp/settings.go @@ -9,6 +9,8 @@ import ( "net/http" "time" + "github.com/ory/x/otelx" + "github.com/pquerna/otp" "github.com/pquerna/otp/totp" "github.com/tidwall/gjson" @@ -68,6 +70,11 @@ type updateSettingsFlowWithTotpMethod struct { // // swagger:ignore Flow string `json:"flow"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (p *updateSettingsFlowWithTotpMethod) GetFlowID() uuid.UUID { @@ -78,33 +85,36 @@ func (p *updateSettingsFlowWithTotpMethod) SetFlowID(rid uuid.UUID) { p.Flow = rid.String() } -func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (*settings.UpdateContext, error) { +func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (_ *settings.UpdateContext, err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.oidc.strategy.Settings") + defer otelx.End(span, &err) + var p updateSettingsFlowWithTotpMethod ctxUpdate, err := settings.PrepareUpdate(s.d, w, r, f, ss, settings.ContinuityKey(s.SettingsStrategyID()), &p) if errors.Is(err, settings.ErrContinuePreviousAction) { - return ctxUpdate, s.continueSettingsFlow(w, r, ctxUpdate, &p) + return ctxUpdate, s.continueSettingsFlow(ctx, r, ctxUpdate, p) } else if err != nil { - return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, p, err) } if err := s.decodeSettingsFlow(r, &p); err != nil { - return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, p, err) } if p.UnlinkTOTP { // This is a submit so we need to manually set the type to TOTP p.Method = s.SettingsStrategyID() - if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { - return nil, s.handleSettingsError(w, r, ctxUpdate, &p, err) + if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + return nil, s.handleSettingsError(w, r, ctxUpdate, p, err) } } else if err := flow.MethodEnabledAndAllowedFromRequest(r, f.GetFlowName(), s.SettingsStrategyID(), s.d); err != nil { - return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, p, err) } // This does not come from the payload! p.Flow = ctxUpdate.Flow.ID.String() - if err := s.continueSettingsFlow(w, r, ctxUpdate, &p); err != nil { - return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) + if err := s.continueSettingsFlow(ctx, r, ctxUpdate, p); err != nil { + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, p, err) } return ctxUpdate, nil @@ -117,29 +127,27 @@ func (s *Strategy) decodeSettingsFlow(r *http.Request, dest interface{}) error { } return decoderx.NewHTTP().Decode(r, dest, compiler, + decoderx.HTTPKeepRequestBody(true), decoderx.HTTPDecoderAllowedMethods("POST", "GET"), decoderx.HTTPDecoderSetValidatePayloads(true), decoderx.HTTPDecoderJSONFollowsFormFormat(), ) } -func (s *Strategy) continueSettingsFlow( - w http.ResponseWriter, r *http.Request, - ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithTotpMethod, -) error { - if err := flow.MethodEnabledAndAllowed(r.Context(), flow.SettingsFlow, s.SettingsStrategyID(), p.Method, s.d); err != nil { +func (s *Strategy) continueSettingsFlow(ctx context.Context, r *http.Request, ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithTotpMethod) error { + if err := flow.MethodEnabledAndAllowed(ctx, flow.SettingsFlow, s.SettingsStrategyID(), p.Method, s.d); err != nil { return err } - if err := flow.EnsureCSRF(s.d, r, ctxUpdate.Flow.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + if err := flow.EnsureCSRF(s.d, r, ctxUpdate.Flow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { return err } - if ctxUpdate.Session.AuthenticatedAt.Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(r.Context())).Before(time.Now()) { + if ctxUpdate.Session.AuthenticatedAt.Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(ctx)).Before(time.Now()) { return errors.WithStack(settings.NewFlowNeedsReAuth()) } - hasTOTP, err := s.identityHasTOTP(r.Context(), ctxUpdate.Session.IdentityID) + hasTOTP, err := s.identityHasTOTP(ctx, ctxUpdate.Session.IdentityID) if err != nil { return err } @@ -150,9 +158,9 @@ func (s *Strategy) continueSettingsFlow( // 2. TOTP should be added -> we do not have it yet var i *identity.Identity if hasTOTP { - i, err = s.continueSettingsFlowRemoveTOTP(w, r, ctxUpdate, p) + i, err = s.continueSettingsFlowRemoveTOTP(ctx, ctxUpdate, p) } else { - i, err = s.continueSettingsFlowAddTOTP(w, r, ctxUpdate, p) + i, err = s.continueSettingsFlowAddTOTP(ctx, ctxUpdate, p) } if err != nil { @@ -163,7 +171,7 @@ func (s *Strategy) continueSettingsFlow( return nil } -func (s *Strategy) continueSettingsFlowAddTOTP(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithTotpMethod) (*identity.Identity, error) { +func (s *Strategy) continueSettingsFlowAddTOTP(ctx context.Context, ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithTotpMethod) (*identity.Identity, error) { keyURL := gjson.GetBytes(ctxUpdate.Flow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeyURL)).String() if len(keyURL) == 0 { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Could not find they TOTP key in the internal context. This is a code bug and should be reported to https://github.com/ory/kratos/.")) @@ -191,7 +199,7 @@ func (s *Strategy) continueSettingsFlowAddTOTP(w http.ResponseWriter, r *http.Re return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode totp options to JSON: %s", err)) } - i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), ctxUpdate.Session.Identity.ID) + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ctxUpdate.Session.Identity.ID) if err != nil { return nil, err } @@ -207,12 +215,12 @@ func (s *Strategy) continueSettingsFlowAddTOTP(w http.ResponseWriter, r *http.Re return nil, err } - if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(r.Context(), ctxUpdate.Flow); err != nil { + if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(ctx, ctxUpdate.Flow); err != nil { return nil, err } // Since we added the method, it also means that we have authenticated it - if err := s.d.SessionManager().SessionAddAuthenticationMethods(r.Context(), ctxUpdate.Session.ID, session.AuthenticationMethod{ + if err := s.d.SessionManager().SessionAddAuthenticationMethods(ctx, ctxUpdate.Session.ID, session.AuthenticationMethod{ Method: s.ID(), AAL: identity.AuthenticatorAssuranceLevel2, }); err != nil { @@ -222,12 +230,12 @@ func (s *Strategy) continueSettingsFlowAddTOTP(w http.ResponseWriter, r *http.Re return i, nil } -func (s *Strategy) continueSettingsFlowRemoveTOTP(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithTotpMethod) (*identity.Identity, error) { +func (s *Strategy) continueSettingsFlowRemoveTOTP(ctx context.Context, ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithTotpMethod) (*identity.Identity, error) { if !p.UnlinkTOTP { return ctxUpdate.Session.Identity, nil } - i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), ctxUpdate.Session.Identity.ID) + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ctxUpdate.Session.Identity.ID) if err != nil { return nil, err } @@ -242,7 +250,7 @@ func (s *Strategy) identityHasTOTP(ctx context.Context, id uuid.UUID) (bool, err return false, err } - count, err := s.CountActiveMultiFactorCredentials(confidential.Credentials) + count, err := s.CountActiveMultiFactorCredentials(ctx, confidential.Credentials) if err != nil { return false, err } @@ -290,7 +298,7 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity return nil } -func (s *Strategy) handleSettingsError(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithTotpMethod, err error) error { +func (s *Strategy) handleSettingsError(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithTotpMethod, err error) error { // Do not pause flow if the flow type is an API flow as we can't save cookies in those flows. if e := new(settings.FlowNeedsReAuth); errors.As(err, &e) && ctxUpdate.Flow != nil && ctxUpdate.Flow.Type == flow.TypeBrowser { if err := s.d.ContinuityManager().Pause(r.Context(), w, r, settings.ContinuityKey(s.SettingsStrategyID()), settings.ContinuityOptions(p, ctxUpdate.GetSessionIdentity())...); err != nil { diff --git a/selfservice/strategy/totp/settings_test.go b/selfservice/strategy/totp/settings_test.go index 0fd479f1b220..b44cc736a560 100644 --- a/selfservice/strategy/totp/settings_test.go +++ b/selfservice/strategy/totp/settings_test.go @@ -64,7 +64,7 @@ func TestCompleteSettings(t *testing.T) { t.Run("case=device unlinking is available when identity has totp", func(t *testing.T) { id, _, _ := createIdentity(t, reg) - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ "0.attributes.value", @@ -76,7 +76,7 @@ func TestCompleteSettings(t *testing.T) { id.Credentials = nil require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), id)) - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ "0.attributes.value", @@ -87,7 +87,7 @@ func TestCompleteSettings(t *testing.T) { }) doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) { - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) values.Set("method", "totp") @@ -97,7 +97,7 @@ func TestCompleteSettings(t *testing.T) { } doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) { - browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS) values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) values.Set("method", "totp") @@ -241,6 +241,7 @@ func TestCompleteSettings(t *testing.T) { assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) checkIdentity(t, id) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) }) t.Run("type=spa", func(t *testing.T) { @@ -250,6 +251,9 @@ func TestCompleteSettings(t *testing.T) { assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) checkIdentity(t, id) + + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", actual) }) t.Run("type=browser", func(t *testing.T) { @@ -259,6 +263,7 @@ func TestCompleteSettings(t *testing.T) { assert.Contains(t, res.Request.URL.String(), uiTS.URL) assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual) checkIdentity(t, id) + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) }) }) @@ -344,12 +349,19 @@ func TestCompleteSettings(t *testing.T) { checkIdentity(t, id, key) testhelpers.EnsureAAL(t, hc, publicTS, "aal2", string(identity.CredentialsTypeTOTP)) + + if isSPA { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", actual) + } else { + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) + } } t.Run("type=api", func(t *testing.T) { id := createIdentityWithoutTOTP(t, reg) - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) run(t, true, false, id, apiClient, f) @@ -358,7 +370,7 @@ func TestCompleteSettings(t *testing.T) { t.Run("type=spa", func(t *testing.T) { id := createIdentityWithoutTOTP(t, reg) - user := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + user := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaBrowser(t, user, true, publicTS) run(t, false, true, id, user, f) @@ -367,7 +379,7 @@ func TestCompleteSettings(t *testing.T) { t.Run("type=browser", func(t *testing.T) { id := createIdentityWithoutTOTP(t, reg) - user := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + user := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaBrowser(t, user, false, publicTS) run(t, false, false, id, user, f) diff --git a/selfservice/strategy/totp/strategy.go b/selfservice/strategy/totp/strategy.go index 6c3205abd9ac..d4f30ba09c67 100644 --- a/selfservice/strategy/totp/strategy.go +++ b/selfservice/strategy/totp/strategy.go @@ -35,6 +35,7 @@ type totpStrategyDependencies interface { x.WriterProvider x.CSRFTokenGeneratorProvider x.CSRFProvider + x.TracingProvider config.Provider @@ -80,11 +81,11 @@ func NewStrategy(d any) *Strategy { } } -func (s *Strategy) CountActiveFirstFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { +func (s *Strategy) CountActiveFirstFactorCredentials(_ context.Context, _ map[identity.CredentialsType]identity.Credentials) (count int, err error) { return 0, nil } -func (s *Strategy) CountActiveMultiFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { +func (s *Strategy) CountActiveMultiFactorCredentials(_ context.Context, cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { for _, c := range cc { if c.Type == s.ID() && len(c.Config) > 0 { var conf identity.CredentialsTOTPConfig @@ -93,7 +94,7 @@ func (s *Strategy) CountActiveMultiFactorCredentials(cc map[identity.Credentials } _, err := otp.NewKeyFromURL(conf.TOTPURL) - if len(c.Identifiers) > 0 && len(c.Identifiers[0]) > 0 && len(conf.TOTPURL) > 0 && err == nil { + if len(conf.TOTPURL) > 0 && err == nil { count++ } } @@ -109,7 +110,7 @@ func (s *Strategy) NodeGroup() node.UiNodeGroup { return node.TOTPGroup } -func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context, _ session.AuthenticationMethods) session.AuthenticationMethod { +func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.AuthenticationMethod { return session.AuthenticationMethod{ Method: s.ID(), AAL: identity.AuthenticatorAssuranceLevel2, diff --git a/selfservice/strategy/totp/strategy_test.go b/selfservice/strategy/totp/strategy_test.go index 17c507c9d144..7bce7fe16961 100644 --- a/selfservice/strategy/totp/strategy_test.go +++ b/selfservice/strategy/totp/strategy_test.go @@ -25,7 +25,7 @@ func TestCountActiveCredentials(t *testing.T) { require.NoError(t, err) t.Run("first factor", func(t *testing.T) { - actual, err := strategy.CountActiveFirstFactorCredentials(nil) + actual, err := strategy.CountActiveFirstFactorCredentials(nil, nil) require.NoError(t, err) assert.Equal(t, 0, actual) }) @@ -75,7 +75,7 @@ func TestCountActiveCredentials(t *testing.T) { cc[c.Type] = c } - actual, err := strategy.CountActiveMultiFactorCredentials(cc) + actual, err := strategy.CountActiveMultiFactorCredentials(nil, cc) require.NoError(t, err) assert.Equal(t, tc.expected, actual) }) diff --git a/selfservice/strategy/webauthn/.schema/login.schema.json b/selfservice/strategy/webauthn/.schema/login.schema.json index b548b0d9e98e..3029101023ad 100644 --- a/selfservice/strategy/webauthn/.schema/login.schema.json +++ b/selfservice/strategy/webauthn/.schema/login.schema.json @@ -15,6 +15,10 @@ "identifier": { "type": "string", "minLength": 1 + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } }, "if": { diff --git a/selfservice/strategy/webauthn/.schema/settings.schema.json b/selfservice/strategy/webauthn/.schema/settings.schema.json index cca82040fabf..f89ec987e37f 100644 --- a/selfservice/strategy/webauthn/.schema/settings.schema.json +++ b/selfservice/strategy/webauthn/.schema/settings.schema.json @@ -17,6 +17,10 @@ }, "webauthn_remove": { "type": "string" + }, + "transient_payload": { + "type": "object", + "additionalProperties": true } }, "if": { diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json index 0e2e343e00e0..b180cf04a403 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json @@ -24,25 +24,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -61,7 +42,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -70,5 +51,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclickTrigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Sign in with hardware key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json index e6376bbf7d9e..399562e7015d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json @@ -37,7 +37,7 @@ "async": true, "referrerpolicy": "no-referrer", "crossorigin": "anonymous", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "type": "text/javascript", "node_type": "script" }, @@ -51,6 +51,7 @@ "name": "webauthn_login_trigger", "type": "button", "disabled": false, + "onclickTrigger": "oryWebAuthnLogin", "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json index e6376bbf7d9e..399562e7015d 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json @@ -37,7 +37,7 @@ "async": true, "referrerpolicy": "no-referrer", "crossorigin": "anonymous", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "type": "text/javascript", "node_type": "script" }, @@ -51,6 +51,7 @@ "name": "webauthn_login_trigger", "type": "button", "disabled": false, + "onclickTrigger": "oryWebAuthnLogin", "node_type": "input" }, "messages": [], diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=webauthn_button_exists.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=webauthn_button_exists.json index 2405a57aaf04..052fc466dc5b 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=webauthn_button_exists.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=webauthn_button_exists.json @@ -14,6 +14,7 @@ }, { "attributes": { + "autocomplete": "username webauthn", "disabled": false, "name": "identifier", "node_type": "input", @@ -45,7 +46,7 @@ "meta": { "label": { "id": 1010008, - "text": "Use security key", + "text": "Sign in with hardware key", "type": "info" } }, diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json deleted file mode 100644 index 8b27f6ca0daf..000000000000 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json +++ /dev/null @@ -1,75 +0,0 @@ -[ - { - "attributes": { - "disabled": false, - "name": "csrf_token", - "node_type": "input", - "required": true, - "type": "hidden" - }, - "group": "default", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "identifier", - "node_type": "input", - "type": "hidden", - "value": "foo@bar.com" - }, - "group": "default", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login", - "node_type": "input", - "type": "hidden", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "async": true, - "crossorigin": "anonymous", - "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", - "node_type": "script", - "referrerpolicy": "no-referrer", - "type": "text/javascript" - }, - "group": "webauthn", - "messages": [], - "meta": {}, - "type": "script" - } -] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json deleted file mode 100644 index 8b27f6ca0daf..000000000000 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json +++ /dev/null @@ -1,75 +0,0 @@ -[ - { - "attributes": { - "disabled": false, - "name": "csrf_token", - "node_type": "input", - "required": true, - "type": "hidden" - }, - "group": "default", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "identifier", - "node_type": "input", - "type": "hidden", - "value": "foo@bar.com" - }, - "group": "default", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login", - "node_type": "input", - "type": "hidden", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "async": true, - "crossorigin": "anonymous", - "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", - "node_type": "script", - "referrerpolicy": "no-referrer", - "type": "text/javascript" - }, - "group": "webauthn", - "messages": [], - "meta": {}, - "type": "script" - } -] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json deleted file mode 100644 index 8b27f6ca0daf..000000000000 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json +++ /dev/null @@ -1,75 +0,0 @@ -[ - { - "attributes": { - "disabled": false, - "name": "csrf_token", - "node_type": "input", - "required": true, - "type": "hidden" - }, - "group": "default", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "identifier", - "node_type": "input", - "type": "hidden", - "value": "foo@bar.com" - }, - "group": "default", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login", - "node_type": "input", - "type": "hidden", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "async": true, - "crossorigin": "anonymous", - "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", - "node_type": "script", - "referrerpolicy": "no-referrer", - "type": "text/javascript" - }, - "group": "webauthn", - "messages": [], - "meta": {}, - "type": "script" - } -] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json deleted file mode 100644 index 8b27f6ca0daf..000000000000 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json +++ /dev/null @@ -1,75 +0,0 @@ -[ - { - "attributes": { - "disabled": false, - "name": "csrf_token", - "node_type": "input", - "required": true, - "type": "hidden" - }, - "group": "default", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "identifier", - "node_type": "input", - "type": "hidden", - "value": "foo@bar.com" - }, - "group": "default", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login", - "node_type": "input", - "type": "hidden", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "async": true, - "crossorigin": "anonymous", - "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", - "node_type": "script", - "referrerpolicy": "no-referrer", - "type": "text/javascript" - }, - "group": "webauthn", - "messages": [], - "meta": {}, - "type": "script" - } -] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json deleted file mode 100644 index 8b27f6ca0daf..000000000000 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json +++ /dev/null @@ -1,75 +0,0 @@ -[ - { - "attributes": { - "disabled": false, - "name": "csrf_token", - "node_type": "input", - "required": true, - "type": "hidden" - }, - "group": "default", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "identifier", - "node_type": "input", - "type": "hidden", - "value": "foo@bar.com" - }, - "group": "default", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login", - "node_type": "input", - "type": "hidden", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "async": true, - "crossorigin": "anonymous", - "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", - "node_type": "script", - "referrerpolicy": "no-referrer", - "type": "text/javascript" - }, - "group": "webauthn", - "messages": [], - "meta": {}, - "type": "script" - } -] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json deleted file mode 100644 index 8b27f6ca0daf..000000000000 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json +++ /dev/null @@ -1,75 +0,0 @@ -[ - { - "attributes": { - "disabled": false, - "name": "csrf_token", - "node_type": "input", - "required": true, - "type": "hidden" - }, - "group": "default", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "identifier", - "node_type": "input", - "type": "hidden", - "value": "foo@bar.com" - }, - "group": "default", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login", - "node_type": "input", - "type": "hidden", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "async": true, - "crossorigin": "anonymous", - "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", - "node_type": "script", - "referrerpolicy": "no-referrer", - "type": "text/javascript" - }, - "group": "webauthn", - "messages": [], - "meta": {}, - "type": "script" - } -] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json deleted file mode 100644 index 8b27f6ca0daf..000000000000 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json +++ /dev/null @@ -1,75 +0,0 @@ -[ - { - "attributes": { - "disabled": false, - "name": "csrf_token", - "node_type": "input", - "required": true, - "type": "hidden" - }, - "group": "default", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "identifier", - "node_type": "input", - "type": "hidden", - "value": "foo@bar.com" - }, - "group": "default", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login", - "node_type": "input", - "type": "hidden", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "async": true, - "crossorigin": "anonymous", - "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", - "node_type": "script", - "referrerpolicy": "no-referrer", - "type": "text/javascript" - }, - "group": "webauthn", - "messages": [], - "meta": {}, - "type": "script" - } -] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json deleted file mode 100644 index 8b27f6ca0daf..000000000000 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json +++ /dev/null @@ -1,75 +0,0 @@ -[ - { - "attributes": { - "disabled": false, - "name": "csrf_token", - "node_type": "input", - "required": true, - "type": "hidden" - }, - "group": "default", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "identifier", - "node_type": "input", - "type": "hidden", - "value": "foo@bar.com" - }, - "group": "default", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login", - "node_type": "input", - "type": "hidden", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": {}, - "type": "input" - }, - { - "attributes": { - "async": true, - "crossorigin": "anonymous", - "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", - "node_type": "script", - "referrerpolicy": "no-referrer", - "type": "text/javascript" - }, - "group": "webauthn", - "messages": [], - "meta": {}, - "type": "script" - } -] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_credentials-browser.json similarity index 85% rename from selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json rename to selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_credentials-browser.json index 8b27f6ca0daf..5021f44d2f94 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_credentials-browser.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclickTrigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Sign in with hardware key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_credentials-spa.json similarity index 85% rename from selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json rename to selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_credentials-spa.json index 8b27f6ca0daf..5021f44d2f94 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_credentials-spa.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclickTrigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Sign in with hardware key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_credentials-browser.json similarity index 85% rename from selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json rename to selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_credentials-browser.json index 8b27f6ca0daf..5021f44d2f94 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_credentials-browser.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclickTrigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Sign in with hardware key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_credentials-spa.json similarity index 85% rename from selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json rename to selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_credentials-spa.json index 8b27f6ca0daf..5021f44d2f94 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_credentials-spa.json @@ -25,25 +25,6 @@ "meta": {}, "type": "input" }, - { - "attributes": { - "disabled": false, - "name": "webauthn_login_trigger", - "node_type": "input", - "type": "button", - "value": "" - }, - "group": "webauthn", - "messages": [], - "meta": { - "label": { - "id": 1010008, - "text": "Use security key", - "type": "info" - } - }, - "type": "input" - }, { "attributes": { "disabled": false, @@ -62,7 +43,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" @@ -71,5 +52,24 @@ "messages": [], "meta": {}, "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclickTrigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Sign in with hardware key", + "type": "info" + } + }, + "type": "input" } ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=passwordless_credentials-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=passwordless_credentials-browser.json new file mode 100644 index 000000000000..d601ae14020f --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=passwordless_credentials-browser.json @@ -0,0 +1,15 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=passwordless_credentials-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=passwordless_credentials-spa.json new file mode 100644 index 000000000000..d601ae14020f --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=passwordless_credentials-spa.json @@ -0,0 +1,15 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v0_credentials-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v0_credentials-browser.json new file mode 100644 index 000000000000..d601ae14020f --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v0_credentials-browser.json @@ -0,0 +1,15 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v0_credentials-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v0_credentials-spa.json new file mode 100644 index 000000000000..d601ae14020f --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v0_credentials-spa.json @@ -0,0 +1,15 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v1_credentials-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v1_credentials-browser.json new file mode 100644 index 000000000000..d601ae14020f --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v1_credentials-browser.json @@ -0,0 +1,15 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v1_credentials-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v1_credentials-spa.json new file mode 100644 index 000000000000..d601ae14020f --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v1_credentials-spa.json @@ -0,0 +1,15 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=passwordless_credentials-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=passwordless_credentials-browser.json new file mode 100644 index 000000000000..5021f44d2f94 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=passwordless_credentials-browser.json @@ -0,0 +1,75 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "identifier", + "node_type": "input", + "type": "hidden", + "value": "foo@bar.com" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login", + "node_type": "input", + "type": "hidden", + "value": "" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclickTrigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Sign in with hardware key", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=passwordless_credentials-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=passwordless_credentials-spa.json new file mode 100644 index 000000000000..5021f44d2f94 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=passwordless_credentials-spa.json @@ -0,0 +1,75 @@ +[ + { + "attributes": { + "disabled": false, + "name": "csrf_token", + "node_type": "input", + "required": true, + "type": "hidden" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "identifier", + "node_type": "input", + "type": "hidden", + "value": "foo@bar.com" + }, + "group": "default", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login", + "node_type": "input", + "type": "hidden", + "value": "" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "input" + }, + { + "attributes": { + "async": true, + "crossorigin": "anonymous", + "id": "webauthn_script", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", + "node_type": "script", + "referrerpolicy": "no-referrer", + "type": "text/javascript" + }, + "group": "webauthn", + "messages": [], + "meta": {}, + "type": "script" + }, + { + "attributes": { + "disabled": false, + "name": "webauthn_login_trigger", + "node_type": "input", + "onclickTrigger": "oryWebAuthnLogin", + "type": "button" + }, + "group": "webauthn", + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Sign in with hardware key", + "type": "info" + } + }, + "type": "input" + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json index d27075b6063d..f0edfe3c5966 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json @@ -82,33 +82,33 @@ { "attributes": { "disabled": false, - "name": "webauthn_register_trigger", + "name": "webauthn_register", "node_type": "input", - "type": "button", + "type": "hidden", "value": "" }, "group": "webauthn", "messages": [], - "meta": { - "label": { - "id": 1050012, - "text": "Add security key", - "type": "info" - } - }, + "meta": {}, "type": "input" }, { "attributes": { "disabled": false, - "name": "webauthn_register", + "name": "webauthn_register_trigger", "node_type": "input", - "type": "hidden", - "value": "" + "onclickTrigger": "oryWebAuthnRegistration", + "type": "button" }, "group": "webauthn", "messages": [], - "meta": {}, + "meta": { + "label": { + "id": 1050012, + "text": "Add security key", + "type": "info" + } + }, "type": "input" }, { @@ -116,7 +116,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser-response.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser-response.json index ab452afa7ea9..2fdeed6db8b4 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser-response.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser-response.json @@ -1,10 +1,11 @@ { "type": "input", - "group": "default", + "group": "webauthn", "attributes": { "name": "webauthn_remove", - "type": "text", - "disabled": false, + "type": "submit", + "value": "666f6f666f6f", + "disabled": true, "node_type": "input" }, "messages": [ @@ -17,5 +18,16 @@ } } ], - "meta": {} + "meta": { + "label": { + "id": 1050018, + "text": "Remove security key \"foo\"", + "type": "info", + "context": { + "added_at": "0001-01-01T00:00:00Z", + "added_at_unix": -62135596800, + "display_name": "foo" + } + } + } } diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json index 0a9fb6164387..9bd36e752fd0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json @@ -5,7 +5,7 @@ "webauthn_register_displayname": [ "" ], - "webauthn_register_trigger": [ - "" + "webauthn_remove": [ + "666f6f666f6f" ] } diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa-response.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa-response.json index ab452afa7ea9..2fdeed6db8b4 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa-response.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa-response.json @@ -1,10 +1,11 @@ { "type": "input", - "group": "default", + "group": "webauthn", "attributes": { "name": "webauthn_remove", - "type": "text", - "disabled": false, + "type": "submit", + "value": "666f6f666f6f", + "disabled": true, "node_type": "input" }, "messages": [ @@ -17,5 +18,16 @@ } } ], - "meta": {} + "meta": { + "label": { + "id": 1050018, + "text": "Remove security key \"foo\"", + "type": "info", + "context": { + "added_at": "0001-01-01T00:00:00Z", + "added_at_unix": -62135596800, + "display_name": "foo" + } + } + } } diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json index 0a9fb6164387..9bd36e752fd0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json @@ -5,7 +5,7 @@ "webauthn_register_displayname": [ "" ], - "webauthn_register_trigger": [ - "" + "webauthn_remove": [ + "666f6f666f6f" ] } diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json index ab20337abc14..c15a847d4703 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json @@ -34,33 +34,33 @@ { "attributes": { "disabled": false, - "name": "webauthn_register_trigger", + "name": "webauthn_register", "node_type": "input", - "type": "button", + "type": "hidden", "value": "" }, "group": "webauthn", "messages": [], - "meta": { - "label": { - "id": 1050012, - "text": "Add security key", - "type": "info" - } - }, + "meta": {}, "type": "input" }, { "attributes": { "disabled": false, - "name": "webauthn_register", + "name": "webauthn_register_trigger", "node_type": "input", - "type": "hidden", - "value": "" + "onclickTrigger": "oryWebAuthnRegistration", + "type": "button" }, "group": "webauthn", "messages": [], - "meta": {}, + "meta": { + "label": { + "id": 1050012, + "text": "Add security key", + "type": "info" + } + }, "type": "input" }, { @@ -68,7 +68,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json index 515658a3d64f..9bd36e752fd0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json @@ -5,9 +5,6 @@ "webauthn_register_displayname": [ "" ], - "webauthn_register_trigger": [ - "" - ], "webauthn_remove": [ "666f6f666f6f" ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json index 515658a3d64f..9bd36e752fd0 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json @@ -5,9 +5,6 @@ "webauthn_register_displayname": [ "" ], - "webauthn_register_trigger": [ - "" - ], "webauthn_remove": [ "666f6f666f6f" ] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=mfa_enabled.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=mfa_enabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=passwordless_enabled.json new file mode 100644 index 000000000000..ddd8316aa00f --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=passwordless_enabled.json @@ -0,0 +1,54 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "text", + "required": true, + "autocomplete": "username webauthn", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "method", + "type": "submit", + "value": "webauthn", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Sign in with hardware key", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled-case=account_enumeration_mitigation_disabled.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled-case=account_enumeration_mitigation_disabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled-case=account_enumeration_mitigation_enabled.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled-case=account_enumeration_mitigation_enabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=passwordless_enabled-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=passwordless_enabled-case=account_enumeration_mitigation_disabled.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=passwordless_enabled-case=account_enumeration_mitigation_disabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=passwordless_enabled-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=passwordless_enabled-case=account_enumeration_mitigation_enabled.json new file mode 100644 index 000000000000..63ce82315a77 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=passwordless_enabled-case=account_enumeration_mitigation_enabled.json @@ -0,0 +1,34 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "method", + "type": "submit", + "value": "webauthn", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Sign in with hardware key", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_webauthn-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_webauthn-case=mfa_enabled.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_webauthn-case=mfa_enabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_webauthn-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_webauthn-case=passwordless_enabled.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_webauthn-case=passwordless_enabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_webauthn-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_webauthn-case=mfa_enabled.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_webauthn-case=mfa_enabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_webauthn-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_webauthn-case=passwordless_enabled.json new file mode 100644 index 000000000000..63ce82315a77 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_webauthn-case=passwordless_enabled.json @@ -0,0 +1,34 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "method", + "type": "submit", + "value": "webauthn", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Sign in with hardware key", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=mfa_enabled.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=mfa_enabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=passwordless_enabled.json new file mode 100644 index 000000000000..63ce82315a77 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=passwordless_enabled.json @@ -0,0 +1,34 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "method", + "type": "submit", + "value": "webauthn", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Sign in with hardware key", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled-case=account_enumeration_mitigation_disabled.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled-case=account_enumeration_mitigation_disabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled-case=account_enumeration_mitigation_enabled.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled-case=account_enumeration_mitigation_enabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=passwordless_enabled-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=passwordless_enabled-case=account_enumeration_mitigation_disabled.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=passwordless_enabled-case=account_enumeration_mitigation_disabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=passwordless_enabled-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=passwordless_enabled-case=account_enumeration_mitigation_enabled.json new file mode 100644 index 000000000000..63ce82315a77 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=passwordless_enabled-case=account_enumeration_mitigation_enabled.json @@ -0,0 +1,34 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "method", + "type": "submit", + "value": "webauthn", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Sign in with hardware key", + "type": "info" + } + } + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification-case=mfa_enabled.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification-case=mfa_enabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification-case=passwordless_enabled.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification-case=passwordless_enabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=mfa_enabled_and_user_has_mfa_credentials.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=mfa_enabled_and_user_has_mfa_credentials.json new file mode 100644 index 000000000000..1be62bb13f42 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=mfa_enabled_and_user_has_mfa_credentials.json @@ -0,0 +1,74 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "script", + "group": "webauthn", + "attributes": { + "async": true, + "referrerpolicy": "no-referrer", + "crossorigin": "anonymous", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "webauthn_login_trigger", + "type": "button", + "disabled": false, + "onclickTrigger": "oryWebAuthnLogin", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Sign in with hardware key", + "type": "info" + } + } + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "webauthn_login", + "type": "hidden", + "value": "", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=mfa_enabled_but_user_has_passwordless_credentials.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=mfa_enabled_but_user_has_passwordless_credentials.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=mfa_enabled_but_user_has_passwordless_credentials.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=passwordless_enabled_and_user_has_passwordless_credentials.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=passwordless_enabled_and_user_has_passwordless_credentials.json new file mode 100644 index 000000000000..1be62bb13f42 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=passwordless_enabled_and_user_has_passwordless_credentials.json @@ -0,0 +1,74 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "script", + "group": "webauthn", + "attributes": { + "async": true, + "referrerpolicy": "no-referrer", + "crossorigin": "anonymous", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "webauthn_login_trigger", + "type": "button", + "disabled": false, + "onclickTrigger": "oryWebAuthnLogin", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Sign in with hardware key", + "type": "info" + } + } + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "webauthn_login", + "type": "hidden", + "value": "", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=passwordless_enabled_but_user_has_no_passwordless_credentials.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=passwordless_enabled_but_user_has_no_passwordless_credentials.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=passwordless_enabled_but_user_has_no_passwordless_credentials.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=mfa_enabled.json new file mode 100644 index 000000000000..1be62bb13f42 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=mfa_enabled.json @@ -0,0 +1,74 @@ +[ + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "script", + "group": "webauthn", + "attributes": { + "async": true, + "referrerpolicy": "no-referrer", + "crossorigin": "anonymous", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", + "type": "text/javascript", + "id": "webauthn_script", + "node_type": "script" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "webauthn_login_trigger", + "type": "button", + "disabled": false, + "onclickTrigger": "oryWebAuthnLogin", + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010008, + "text": "Sign in with hardware key", + "type": "info" + } + } + }, + { + "type": "input", + "group": "webauthn", + "attributes": { + "name": "webauthn_login", + "type": "hidden", + "value": "", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + } +] diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=passwordless_enabled.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=passwordless_enabled.json @@ -0,0 +1 @@ +[] diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json index 8242108ee80d..58708f949058 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json @@ -75,8 +75,8 @@ "disabled": false, "name": "webauthn_register_trigger", "node_type": "input", - "type": "button", - "value": "" + "onclickTrigger": "oryWebAuthnRegistration", + "type": "button" }, "group": "webauthn", "messages": [], @@ -94,7 +94,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json index 8242108ee80d..58708f949058 100644 --- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json +++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json @@ -75,8 +75,8 @@ "disabled": false, "name": "webauthn_register_trigger", "node_type": "input", - "type": "button", - "value": "" + "onclickTrigger": "oryWebAuthnRegistration", + "type": "button" }, "group": "webauthn", "messages": [], @@ -94,7 +94,7 @@ "async": true, "crossorigin": "anonymous", "id": "webauthn_script", - "integrity": "sha512-RI23aG5lwYTo7zknGdc++eHUMimUWhkyFzrGid6HkVSdUSjdESPtM3KufXGq/lo4Ut0jI9mDiZeT8tHoSvaHvg==", + "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==", "node_type": "script", "referrerpolicy": "no-referrer", "type": "text/javascript" diff --git a/selfservice/strategy/webauthn/fixtures/registration/failure/internal_context_missing_user_id.json b/selfservice/strategy/webauthn/fixtures/registration/failure/internal_context_missing_user_id.json new file mode 100644 index 000000000000..1f4913b5375e --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/registration/failure/internal_context_missing_user_id.json @@ -0,0 +1,7 @@ +{ + "webauthn_session_data": { + "challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY", + "user_id": "", + "userVerification": "" + } +} diff --git a/selfservice/strategy/webauthn/fixtures/registration/failure/internal_context_wrong_user_id.json b/selfservice/strategy/webauthn/fixtures/registration/failure/internal_context_wrong_user_id.json new file mode 100644 index 000000000000..6eaa968b6f56 --- /dev/null +++ b/selfservice/strategy/webauthn/fixtures/registration/failure/internal_context_wrong_user_id.json @@ -0,0 +1,7 @@ +{ + "webauthn_session_data": { + "challenge": "UlxHSTkuMvtVDoV9y5lhu9OyNUP8P7MP0RYAT6Im_rY", + "user_id": "wrong", + "userVerification": "" + } +} diff --git a/selfservice/strategy/webauthn/js/webauthn.js b/selfservice/strategy/webauthn/js/webauthn.js deleted file mode 100644 index 44c389d6f16b..000000000000 --- a/selfservice/strategy/webauthn/js/webauthn.js +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -;(function () { - if (!window) { - return - } - - if (!window.PublicKeyCredential) { - alert("This browser does not support WebAuthn!") - } - - if (window.__oryWebAuthnInitialized) { - return - } - - function __oryWebAuthnBufferDecode(value) { - return Uint8Array.from( - atob(value.replaceAll("-", "+").replaceAll("_", "/")), - function (c) { - return c.charCodeAt(0) - }, - ) - } - - function __oryWebAuthnBufferEncode(value) { - return btoa(String.fromCharCode.apply(null, new Uint8Array(value))) - .replaceAll("+", "-") - .replaceAll("/", "_") - .replaceAll("=", "") - } - - function __oryWebAuthnLogin( - opt, - resultQuerySelector = '*[name="webauthn_login"]', - triggerQuerySelector = '*[name="webauthn_login_trigger"]', - ) { - if (!window.PublicKeyCredential) { - alert("This browser does not support WebAuthn!") - } - - opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) - opt.publicKey.allowCredentials = opt.publicKey.allowCredentials.map( - function (value) { - return { - ...value, - id: __oryWebAuthnBufferDecode(value.id), - } - }, - ) - - navigator.credentials - .get(opt) - .then(function (credential) { - document.querySelector(resultQuerySelector).value = JSON.stringify({ - id: credential.id, - rawId: __oryWebAuthnBufferEncode(credential.rawId), - type: credential.type, - response: { - authenticatorData: __oryWebAuthnBufferEncode( - credential.response.authenticatorData, - ), - clientDataJSON: __oryWebAuthnBufferEncode( - credential.response.clientDataJSON, - ), - signature: __oryWebAuthnBufferEncode(credential.response.signature), - userHandle: __oryWebAuthnBufferEncode( - credential.response.userHandle, - ), - }, - }) - - document.querySelector(triggerQuerySelector).closest("form").submit() - }) - .catch((err) => { - alert(err) - }) - } - - function __oryWebAuthnRegistration( - opt, - resultQuerySelector = '*[name="webauthn_register"]', - triggerQuerySelector = '*[name="webauthn_register_trigger"]', - ) { - if (!window.PublicKeyCredential) { - alert("This browser does not support WebAuthn!") - } - - opt.publicKey.user.id = __oryWebAuthnBufferDecode(opt.publicKey.user.id) - opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) - - if (opt.publicKey.excludeCredentials) { - opt.publicKey.excludeCredentials = opt.publicKey.excludeCredentials.map( - function (value) { - return { - ...value, - id: __oryWebAuthnBufferDecode(value.id), - } - }, - ) - } - - navigator.credentials - .create(opt) - .then(function (credential) { - document.querySelector(resultQuerySelector).value = JSON.stringify({ - id: credential.id, - rawId: __oryWebAuthnBufferEncode(credential.rawId), - type: credential.type, - response: { - attestationObject: __oryWebAuthnBufferEncode( - credential.response.attestationObject, - ), - clientDataJSON: __oryWebAuthnBufferEncode( - credential.response.clientDataJSON, - ), - }, - }) - - document.querySelector(triggerQuerySelector).closest("form").submit() - }) - .catch((err) => { - alert(err) - }) - } - - window["__oryWebAuthnLogin"] = __oryWebAuthnLogin - window["__oryWebAuthnRegistration"] = __oryWebAuthnRegistration - window["__oryWebAuthnInitialized"] = true -})() diff --git a/selfservice/strategy/webauthn/login.go b/selfservice/strategy/webauthn/login.go index 38cdf283b9f1..97fdd1190ab2 100644 --- a/selfservice/strategy/webauthn/login.go +++ b/selfservice/strategy/webauthn/login.go @@ -4,13 +4,22 @@ package webauthn import ( + "context" "encoding/json" "net/http" "strings" "time" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/x/otelx" + + "github.com/ory/kratos/selfservice/strategy/idfirst" + "github.com/ory/kratos/selfservice/flowhelpers" "github.com/ory/kratos/session" + "github.com/ory/kratos/x/webauthnx" "github.com/gofrs/uuid" @@ -18,8 +27,6 @@ import ( "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" - "github.com/ory/x/urlx" - "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" "github.com/tidwall/gjson" @@ -35,73 +42,14 @@ import ( "github.com/ory/x/decoderx" ) -func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *login.Flow) error { - if sr.Type != flow.TypeBrowser { - return nil - } +var _ login.FormHydrator = new(Strategy) - if s.d.Config().WebAuthnForPasswordless(r.Context()) && (requestedAAL == identity.AuthenticatorAssuranceLevel1) { - if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, ErrNoCredentials) { - return nil - } else if err != nil { - return err - } - return nil - } else if sr.IsForced() { - if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, ErrNoCredentials) { - return nil - } else if err != nil { - return err - } - return nil - } else if !s.d.Config().WebAuthnForPasswordless(r.Context()) && (requestedAAL == identity.AuthenticatorAssuranceLevel2) { - // We have done proper validation before so this should never error - sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r) - if err != nil { - return err - } - - if err := s.populateLoginMethod(r, sr, sess.Identity, text.NewInfoSelfServiceLoginWebAuthn(), identity.AuthenticatorAssuranceLevel2); errors.Is(err, ErrNoCredentials) { - return nil - } else if err != nil { - return err - } - - return nil - } - - return nil +func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) { + webauthnx.RegisterWebauthnRoute(r) } func (s *Strategy) populateLoginMethodForPasswordless(r *http.Request, sr *login.Flow) error { - if sr.IsForced() { - identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID()) - if identifier == "" { - return nil - } - - if err := s.populateLoginMethod(r, sr, id, text.NewInfoSelfServiceLoginWebAuthn(), ""); errors.Is(err, ErrNoCredentials) { - return nil - } else if err != nil { - return err - } - - sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden)) - return nil - } - - ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) - if err != nil { - return err - } - identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) - if err != nil { - return err - } - sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - sr.UI.SetNode(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel)) sr.UI.GetNodes().Append(node.NewInputField("method", "webauthn", node.WebAuthnGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceLoginWebAuthn())) return nil } @@ -123,14 +71,10 @@ func (s *Strategy) populateLoginMethod(r *http.Request, sr *login.Flow, i *ident return errors.WithStack(err) } - webAuthCreds := conf.Credentials.ToWebAuthn() - if !sr.IsForced() { - webAuthCreds = conf.Credentials.ToWebAuthnFiltered(aal) - } - + webAuthCreds := conf.Credentials.ToWebAuthnFiltered(aal, nil) if len(webAuthCreds) == 0 { // Identity has no webauthn - return ErrNoCredentials + return webauthnx.ErrNoCredentials } web, err := webauthn.New(s.d.Config().WebAuthnConfig(r.Context())) @@ -138,7 +82,7 @@ func (s *Strategy) populateLoginMethod(r *http.Request, sr *login.Flow, i *ident return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initiate WebAuth.").WithDebug(err.Error())) } - options, sessionData, err := web.BeginLogin(NewUser(conf.UserHandle, webAuthCreds, web.Config)) + options, sessionData, err := web.BeginLogin(webauthnx.NewUser(conf.UserHandle, webAuthCreds, web.Config)) if err != nil { return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initiate WebAuth login.").WithDebug(err.Error())) } @@ -159,10 +103,10 @@ func (s *Strategy) populateLoginMethod(r *http.Request, sr *login.Flow, i *ident } sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) - sr.UI.Nodes.Upsert(NewWebAuthnScript(urlx.AppendPaths(s.d.Config().SelfPublicURL(r.Context()), webAuthnRoute).String(), jsOnLoad)) - sr.UI.SetNode(NewWebAuthnLoginTrigger(string(injectWebAuthnOptions)). + sr.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(r.Context()))) + sr.UI.SetNode(webauthnx.NewWebAuthnLoginTrigger(string(injectWebAuthnOptions)). WithMetaLabel(label)) - sr.UI.Nodes.Upsert(NewWebAuthnLoginInput()) + sr.UI.Nodes.Upsert(webauthnx.NewWebAuthnLoginInput()) return nil } @@ -199,49 +143,65 @@ type updateLoginFlowWithWebAuthnMethod struct { // // This must contain the ID of the WebAuthN connection. Login string `json:"webauthn_login"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, sess *session.Session) (i *identity.Identity, err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.webauthn.strategy.Login") + defer otelx.End(span, &err) + if f.Type != flow.TypeBrowser { + span.SetAttributes(attribute.String("not_responsible_reason", "flow type is not browser")) return nil, flow.ErrStrategyNotResponsible } var p updateLoginFlowWithWebAuthnMethod if err := s.hd.Decode(r, &p, + decoderx.HTTPKeepRequestBody(true), decoderx.HTTPDecoderSetValidatePayloads(true), decoderx.MustHTTPRawJSONSchemaCompiler(loginSchema), decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { return nil, s.handleLoginError(r, f, err) } + f.TransientPayload = p.TransientPayload if len(p.Login) > 0 || p.Method == s.SettingsStrategyID() { // This method has only two submit buttons p.Method = s.SettingsStrategyID() } else { + span.SetAttributes(attribute.String("not_responsible_reason", "login is not provided and method is not webauthn")) return nil, flow.ErrStrategyNotResponsible } - if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { return nil, s.handleLoginError(r, f, err) } - if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { return nil, s.handleLoginError(r, f, err) } - if s.d.Config().WebAuthnForPasswordless(r.Context()) || f.IsForced() && f.RequestedAAL == identity.AuthenticatorAssuranceLevel1 { - return s.loginPasswordless(w, r, f, &p) + if s.d.Config().WebAuthnForPasswordless(ctx) || f.IsRefresh() && f.RequestedAAL == identity.AuthenticatorAssuranceLevel1 { + return s.loginPasswordless(ctx, w, r, f, &p) } - return s.loginMultiFactor(w, r, f, sess.IdentityID, &p) + return s.loginMultiFactor(ctx, r, f, sess.IdentityID, &p) } -func (s *Strategy) loginPasswordless(w http.ResponseWriter, r *http.Request, f *login.Flow, p *updateLoginFlowWithWebAuthnMethod) (i *identity.Identity, err error) { +func (s *Strategy) loginPasswordless(ctx context.Context, w http.ResponseWriter, r *http.Request, f *login.Flow, p *updateLoginFlowWithWebAuthnMethod) (i *identity.Identity, err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.webauthn.strategy.loginPasswordless") + defer otelx.End(span, &err) + if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil { + span.SetAttributes(attribute.String("not_responsible_reason", "requested AAL is not AAL1")) return nil, s.handleLoginError(r, f, err) } - if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { return nil, s.handleLoginError(r, f, err) } @@ -249,9 +209,9 @@ func (s *Strategy) loginPasswordless(w http.ResponseWriter, r *http.Request, f * return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrBadRequest.WithReason("identifier is required"))) } - i, _, err = s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), s.ID(), p.Identifier) + i, _, err = s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, s.ID(), p.Identifier) if err != nil { - time.Sleep(x.RandomDelay(s.d.Config().HasherArgon2(r.Context()).ExpectedDuration, s.d.Config().HasherArgon2(r.Context()).ExpectedDeviation)) + time.Sleep(x.RandomDelay(s.d.Config().HasherArgon2(ctx).ExpectedDuration, s.d.Config().HasherArgon2(ctx).ExpectedDeviation)) return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewNoWebAuthnCredentials())) } @@ -261,7 +221,7 @@ func (s *Strategy) loginPasswordless(w http.ResponseWriter, r *http.Request, f * previousNodes := f.UI.Nodes f.UI.Nodes = node.Nodes{} - if err := s.populateLoginMethod(r, f, i, text.NewInfoSelfServiceLoginContinue(), identity.AuthenticatorAssuranceLevel1); errors.Is(err, ErrNoCredentials) { + if err := s.populateLoginMethod(r, f, i, text.NewInfoSelfServiceLoginContinue(), identity.AuthenticatorAssuranceLevel1); errors.Is(err, webauthnx.ErrNoCredentials) { f.UI.Nodes = previousNodes return nil, s.handleLoginError(r, f, schema.NewNoWebAuthnCredentials()) } else if err != nil { @@ -272,25 +232,28 @@ func (s *Strategy) loginPasswordless(w http.ResponseWriter, r *http.Request, f * f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) f.UI.Messages.Add(text.NewInfoLoginWebAuthnPasswordless()) f.UI.SetNode(node.NewInputField("identifier", p.Identifier, node.DefaultGroup, node.InputAttributeTypeHidden, node.WithRequiredInputAttribute)) - if err := s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), f); err != nil { + if err := s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { return nil, s.handleLoginError(r, f, err) } - redirectTo := f.AppendTo(s.d.Config().SelfServiceFlowLoginUI(r.Context())).String() + redirectTo := f.AppendTo(s.d.Config().SelfServiceFlowLoginUI(ctx)).String() if x.IsJSONRequest(r) { s.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(redirectTo)) } else { - http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowLoginUI(r.Context())).String(), http.StatusSeeOther) + http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowLoginUI(ctx)).String(), http.StatusSeeOther) } return nil, errors.WithStack(flow.ErrCompletedByStrategy) } - return s.loginAuthenticate(w, r, f, i.ID, p, identity.AuthenticatorAssuranceLevel1) + return s.loginAuthenticate(ctx, r, f, i.ID, p, identity.AuthenticatorAssuranceLevel1) } -func (s *Strategy) loginAuthenticate(_ http.ResponseWriter, r *http.Request, f *login.Flow, identityID uuid.UUID, p *updateLoginFlowWithWebAuthnMethod, aal identity.AuthenticatorAssuranceLevel) (*identity.Identity, error) { - i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), identityID) +func (s *Strategy) loginAuthenticate(ctx context.Context, r *http.Request, f *login.Flow, identityID uuid.UUID, p *updateLoginFlowWithWebAuthnMethod, aal identity.AuthenticatorAssuranceLevel) (_ *identity.Identity, err error) { + ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.webauthn.strategy.loginAuthenticate") + defer otelx.End(span, &err) + + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, identityID) if err != nil { return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewNoWebAuthnRegistered())) } @@ -305,7 +268,7 @@ func (s *Strategy) loginAuthenticate(_ http.ResponseWriter, r *http.Request, f * return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReason("The WebAuthn credentials could not be decoded properly").WithDebug(err.Error()).WithWrap(err))) } - web, err := webauthn.New(s.d.Config().WebAuthnConfig(r.Context())) + web, err := webauthn.New(s.d.Config().WebAuthnConfig(ctx)) if err != nil { return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error()))) } @@ -320,12 +283,12 @@ func (s *Strategy) loginAuthenticate(_ http.ResponseWriter, r *http.Request, f * return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object but got: %s", err))) } - webAuthCreds := o.Credentials.ToWebAuthnFiltered(aal) - if f.IsForced() { + webAuthCreds := o.Credentials.ToWebAuthnFiltered(aal, &webAuthnResponse.Response.AuthenticatorData.Flags) + if f.IsRefresh() { webAuthCreds = o.Credentials.ToWebAuthn() } - if _, err := web.ValidateLogin(NewUser(o.UserHandle, webAuthCreds, web.Config), webAuthnSess, webAuthnResponse); err != nil { + if _, err := web.ValidateLogin(webauthnx.NewUser(o.UserHandle, webAuthCreds, web.Config), webAuthnSess, webAuthnResponse); err != nil { return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewWebAuthnVerifierWrongError("#/"))) } @@ -336,16 +299,138 @@ func (s *Strategy) loginAuthenticate(_ http.ResponseWriter, r *http.Request, f * } f.Active = s.ID() - if err = s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), f); err != nil { + if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { return nil, s.handleLoginError(r, f, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error()))) } return i, nil } -func (s *Strategy) loginMultiFactor(w http.ResponseWriter, r *http.Request, f *login.Flow, identityID uuid.UUID, p *updateLoginFlowWithWebAuthnMethod) (*identity.Identity, error) { +func (s *Strategy) loginMultiFactor(ctx context.Context, r *http.Request, f *login.Flow, identityID uuid.UUID, p *updateLoginFlowWithWebAuthnMethod) (*identity.Identity, error) { if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel2); err != nil { + trace.SpanFromContext(ctx).SetAttributes(attribute.String("not_responsible_reason", "requested AAL is not AAL2")) return nil, err } - return s.loginAuthenticate(w, r, f, identityID, p, identity.AuthenticatorAssuranceLevel2) + return s.loginAuthenticate(ctx, r, f, identityID, p, identity.AuthenticatorAssuranceLevel2) +} + +func (s *Strategy) populateLoginMethodRefresh(r *http.Request, sr *login.Flow) error { + if sr.Type != flow.TypeBrowser { + return nil + } + + identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID()) + if identifier == "" { + return nil + } + + if err := s.populateLoginMethod(r, sr, id, text.NewInfoSelfServiceLoginWebAuthn(), sr.RequestedAAL); errors.Is(err, webauthnx.ErrNoCredentials) { + return nil + } else if err != nil { + return err + } + + sr.UI.SetCSRF(s.d.GenerateCSRFToken(r)) + sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden)) + return nil +} + +func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, sr *login.Flow) error { + return s.populateLoginMethodRefresh(r, sr) +} + +func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *login.Flow) error { + return s.populateLoginMethodRefresh(r, sr) +} + +func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error { + if sr.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(r.Context()) { + return nil + } + + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + if err != nil { + return err + } + + identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String()) + if err != nil { + return err + } + + sr.UI.SetNode(node.NewInputField( + "identifier", + "", + node.DefaultGroup, + node.InputAttributeTypeText, + node.WithRequiredInputAttribute, + func(attributes *node.InputAttributes) { attributes.Autocomplete = "username webauthn" }, + ).WithMetaLabel(identifierLabel)) + + if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) { + return nil + } else if err != nil { + return err + } + + return nil +} + +func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error { + if sr.Type != flow.TypeBrowser || s.d.Config().WebAuthnForPasswordless(r.Context()) { + return nil + } + + // We have done proper validation before so this should never error + sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r) + if err != nil { + return err + } + + if err := s.populateLoginMethod(r, sr, sess.Identity, text.NewInfoSelfServiceLoginWebAuthn(), identity.AuthenticatorAssuranceLevel2); errors.Is(err, webauthnx.ErrNoCredentials) { + return nil + } else if err != nil { + return err + } + + return nil +} + +func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, sr *login.Flow, opts ...login.FormHydratorModifier) error { + if sr.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(r.Context()) { + return errors.WithStack(idfirst.ErrNoCredentialsFound) + } + + o := login.NewFormHydratorOptions(opts) + + var count int + if o.IdentityHint != nil { + var err error + // If we have an identity hint we can perform identity credentials discovery and + // hide this credential if it should not be included. + if count, err = s.CountActiveFirstFactorCredentials(r.Context(), o.IdentityHint.Credentials); err != nil { + return err + } + } + + if count > 0 || s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) { + if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) { + if !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) { + return errors.WithStack(idfirst.ErrNoCredentialsFound) + } + return nil + } else if err != nil { + return err + } + } + + if count == 0 { + return errors.WithStack(idfirst.ErrNoCredentialsFound) + } + + return nil +} + +func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, sr *login.Flow) error { + return nil } diff --git a/selfservice/strategy/webauthn/login_test.go b/selfservice/strategy/webauthn/login_test.go index f5d332182163..6a98f3b3d383 100644 --- a/selfservice/strategy/webauthn/login_test.go +++ b/selfservice/strategy/webauthn/login_test.go @@ -10,8 +10,12 @@ import ( "fmt" "io" "net/http" + "net/http/httptest" "net/url" "testing" + "time" + + "github.com/ory/kratos/selfservice/strategy/idfirst" "github.com/ory/x/jsonx" @@ -30,6 +34,7 @@ import ( "github.com/tidwall/gjson" "github.com/ory/kratos/driver/config" + configtesthelpers "github.com/ory/kratos/driver/config/testhelpers" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" "github.com/ory/kratos/internal/testhelpers" @@ -153,7 +158,7 @@ func TestCompleteLogin(t *testing.T) { } submitWebAuthnLogin := func(t *testing.T, isSPA bool, id *identity.Identity, contextFixture []byte, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.LoginFlow) { - browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) return submitWebAuthnLoginWithClient(t, isSPA, id, contextFixture, browserClient, cb, opts...) } @@ -163,19 +168,30 @@ func TestCompleteLogin(t *testing.T) { conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, nil) }) - run := func(t *testing.T, id *identity.Identity, context, response []byte, isSPA bool, expectedAAL identity.AuthenticatorAssuranceLevel) { + run := func(t *testing.T, id *identity.Identity, context, response []byte, isSPA bool, expectedAAL identity.AuthenticatorAssuranceLevel, expectTriggers bool) { body, res, f := submitWebAuthnLogin(t, isSPA, id, context, func(values url.Values) { values.Set("identifier", loginFixtureSuccessEmail) values.Set(node.WebAuthnLogin, string(response)) - }, testhelpers.InitFlowWithRefresh()) + }, + testhelpers.InitFlowWithRefresh(), + testhelpers.InitFlowWithAAL(expectedAAL), + ) snapshotx.SnapshotTExcept(t, f.Ui.Nodes, []string{ "0.attributes.value", - "2.attributes.onclick", - "4.attributes.nonce", - "4.attributes.src", + "3.attributes.nonce", + "3.attributes.src", + "4.attributes.value", + "4.attributes.onclick", }) + nodes, err := json.Marshal(f.Ui.Nodes) require.NoError(t, err) + + if !expectTriggers { + assert.Falsef(t, gjson.GetBytes(nodes, "#(attributes.name==identifier)").Exists(), "%s", nodes) + return + } + assert.Equal(t, loginFixtureSuccessEmail, gjson.GetBytes(nodes, "#(attributes.name==identifier).attributes.value").String(), "%s", nodes) prefix := "" @@ -208,40 +224,44 @@ func TestCompleteLogin(t *testing.T) { } for _, tc := range []struct { - creds identity.Credentials - response []byte - context []byte - descript string + creds identity.Credentials + response []byte + context []byte + descript string + expectTriggers bool }{ { creds: identity.Credentials{ Config: loginFixtureSuccessV0Credentials, Version: 0, }, - context: loginFixtureSuccessV0Context, - response: loginFixtureSuccessV0Response, - descript: "mfa v0 credentials", + context: loginFixtureSuccessV0Context, + response: loginFixtureSuccessV0Response, + descript: "mfa v0 credentials", + expectTriggers: !e, }, { creds: identity.Credentials{ Config: loginFixtureSuccessV1Credentials, Version: 1, }, - context: loginFixtureSuccessV1Context, - response: loginFixtureSuccessV1Response, - descript: "mfa v1 credentials", + context: loginFixtureSuccessV1Context, + response: loginFixtureSuccessV1Response, + descript: "mfa v1 credentials", + expectTriggers: !e, }, { creds: identity.Credentials{ Config: loginFixtureSuccessV1PasswordlessCredentials, Version: 1, }, - context: loginFixtureSuccessV1PasswordlessContext, - response: loginFixtureSuccessV1PasswordlessResponse, - descript: "passwordless credentials", + context: loginFixtureSuccessV1PasswordlessContext, + response: loginFixtureSuccessV1PasswordlessResponse, + descript: "passwordless credentials", + expectTriggers: e, }, } { - t.Run(fmt.Sprintf("case=mfa v0 credentials/passwordless enabled=%v", e), func(t *testing.T) { + t.Run(fmt.Sprintf("passwordless enabled=%v/case=%s", e, tc.descript), func(t *testing.T) { id := createIdentityWithWebAuthn(t, tc.creds) for _, f := range []string{ @@ -249,7 +269,7 @@ func TestCompleteLogin(t *testing.T) { "spa", } { t.Run(f, func(t *testing.T) { - run(t, id, tc.context, tc.response, f == "spa", expectedAAL) + run(t, id, tc.context, tc.response, f == "spa", expectedAAL, tc.expectTriggers) }) } }) @@ -264,7 +284,8 @@ func TestCompleteLogin(t *testing.T) { for _, f := range []string{"browser", "spa"} { t.Run(f, func(t *testing.T) { id := identity.NewIdentity("") - client := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + id.NID = x.NewUUID() + client := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) f := testhelpers.InitializeLoginFlowViaBrowser(t, client, publicTS, true, f == "spa", false, false) snapshotx.SnapshotTExcept(t, f.Ui.Nodes, []string{ @@ -317,7 +338,7 @@ func TestCompleteLogin(t *testing.T) { }) t.Run("case=webauthn shows error if user tries to sign in but user has no webauth credentials set up", func(t *testing.T) { - id, subject := createIdentityAndReturnIdentifier(t, reg, nil) + id, subject := createIdentityAndReturnIdentifier(t, ctx, reg, nil) id.DeleteCredentialsType(identity.CredentialsTypeWebAuthn) require.NoError(t, reg.IdentityManager().Update(ctx, id, identity.ManagerAllowWriteProtectedTraits)) @@ -344,7 +365,7 @@ func TestCompleteLogin(t *testing.T) { }) t.Run("case=webauthn MFA credentials can not be used for passwordless login", func(t *testing.T) { - _, subject := createIdentityAndReturnIdentifier(t, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","is_passwordless":false}]}`)) + _, subject := createIdentityAndReturnIdentifier(t, ctx, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","is_passwordless":false}]}`)) payload := func(v url.Values) { v.Set("method", identity.CredentialsTypeWebAuthn.String()) @@ -369,7 +390,7 @@ func TestCompleteLogin(t *testing.T) { }) t.Run("case=should fail if webauthn login is invalid", func(t *testing.T) { - _, subject := createIdentityAndReturnIdentifier(t, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`)) + _, subject := createIdentityAndReturnIdentifier(t, ctx, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`)) doBrowserFlow := func(t *testing.T, spa bool, browserClient *http.Client, opts ...testhelpers.InitFlowWithOption) { f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, spa, false, false, opts...) @@ -446,6 +467,13 @@ func TestCompleteLogin(t *testing.T) { actualFlow, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id)) require.NoError(t, err) assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypeWebAuthn, webauthn.InternalContextKeySessionData))) + + if spa { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", body) + } else { + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) + } } t.Run("type=browser", func(t *testing.T) { @@ -460,25 +488,26 @@ func TestCompleteLogin(t *testing.T) { t.Run("flow=mfa", func(t *testing.T) { t.Run("case=webauthn payload is set when identity has webauthn", func(t *testing.T) { - id := createIdentity(t, reg) + id := createIdentity(t, ctx, reg) - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeLoginFlowViaBrowser(t, apiClient, publicTS, false, true, false, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) assert.Equal(t, gjson.GetBytes(id.Traits, "subject").String(), f.Ui.Nodes[1].Attributes.UiNodeInputAttributes.Value, jsonx.TestMarshalJSONString(t, f.Ui)) testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ "0.attributes.value", "1.attributes.value", - "2.attributes.onclick", - "2.attributes.onload", - "4.attributes.src", - "4.attributes.nonce", + "3.attributes.src", + "3.attributes.nonce", + "4.attributes.onclick", + "4.attributes.onload", + "4.attributes.value", }) - ensureReplacement(t, "2", f.Ui, "allowCredentials") + ensureReplacement(t, "4", f.Ui, "allowCredentials") }) t.Run("case=webauthn payload is not set when identity has no webauthn", func(t *testing.T) { id := createIdentityWithoutWebAuthn(t, reg) - apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) f := testhelpers.InitializeLoginFlowViaBrowser(t, apiClient, publicTS, false, true, false, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ @@ -487,23 +516,23 @@ func TestCompleteLogin(t *testing.T) { }) t.Run("case=webauthn payload is not set for API clients", func(t *testing.T) { - id := createIdentity(t, reg) + id := createIdentity(t, ctx, reg) - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) assertx.EqualAsJSON(t, nil, f.Ui.Nodes) }) doAPIFlowSignedIn := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) { - return doAPIFlow(t, v, testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id), testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) + return doAPIFlow(t, v, testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id), testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) } doBrowserFlowSignIn := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) { - return doBrowserFlow(t, spa, v, testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id), testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) + return doBrowserFlow(t, spa, v, testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id), testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)) } t.Run("case=should refuse to execute api flow", func(t *testing.T) { - id := createIdentity(t, reg) + id := createIdentity(t, ctx, reg) payload := func(v url.Values) { v.Set(node.WebAuthnLogin, "{}") } @@ -515,7 +544,7 @@ func TestCompleteLogin(t *testing.T) { }) t.Run("case=should fail if webauthn login is invalid", func(t *testing.T) { - id, sub := createIdentityAndReturnIdentifier(t, reg, nil) + id, sub := createIdentityAndReturnIdentifier(t, ctx, reg, nil) payload := func(v url.Values) { v.Set("identifier", sub) v.Set(node.WebAuthnLogin, string(loginFixtureSuccessResponseInvalid)) @@ -615,3 +644,284 @@ func TestCompleteLogin(t *testing.T) { }) }) } + +func TestFormHydration(t *testing.T) { + ctx := context.Background() + conf, reg := internal.NewFastRegistryWithMocks(t) + + ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".enabled", true) + ctx = configtesthelpers.WithConfigValue( + ctx, + config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config", + map[string]interface{}{ + "rp": map[string]interface{}{ + "display_name": "foo", + "id": "localhost", + "origins": []string{"http://localhost"}, + }, + }, + ) + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/login.schema.json") + + s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeWebAuthn) + require.NoError(t, err) + fh, ok := s.(login.FormHydrator) + require.True(t, ok) + + toSnapshot := func(t *testing.T, f *login.Flow) { + t.Helper() + // The CSRF token has a unique value that messes with the snapshot - ignore it. + f.UI.Nodes.ResetNodes("csrf_token") + f.UI.Nodes.ResetNodes("identifier") + f.UI.Nodes.ResetNodes("webauthn_login_trigger") + snapshotx.SnapshotT(t, f.UI.Nodes, snapshotx.ExceptNestedKeys("onclick", "nonce", "src")) + } + + newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) { + r := httptest.NewRequest("GET", "/self-service/login/browser", nil) + r = r.WithContext(ctx) + t.Helper() + f, err := login.NewFlow(conf, time.Minute, "csrf_token", r, flow.TypeBrowser) + f.UI.Nodes = make(node.Nodes, 0) + require.NoError(t, err) + return r, f + } + + passwordlessEnabled := configtesthelpers.WithConfigValue(ctx, config.ViperKeyWebAuthnPasswordless, true) + mfaEnabled := configtesthelpers.WithConfigValue(ctx, config.ViperKeyWebAuthnPasswordless, false) + + t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) { + id := createIdentity(t, ctx, reg) + headers := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + t.Run("case=passwordless enabled", func(t *testing.T) { + r, f := newFlow(passwordlessEnabled, t) + + r.Header = headers + f.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + + require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("case=mfa enabled", func(t *testing.T) { + r, f := newFlow(mfaEnabled, t) + + r.Header = headers + f.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + + require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f)) + toSnapshot(t, f) + }) + }) + + t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) { + t.Run("case=passwordless enabled", func(t *testing.T) { + r, f := newFlow(passwordlessEnabled, t) + require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f)) + toSnapshot(t, f) + }) + + t.Run("case=mfa enabled", func(t *testing.T) { + r, f := newFlow(mfaEnabled, t) + require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f)) + toSnapshot(t, f) + }) + }) + + t.Run("method=PopulateLoginMethodRefresh", func(t *testing.T) { + t.Run("case=passwordless enabled but user has no passwordless credentials", func(t *testing.T) { + id := createIdentity(t, ctx, reg) + r, f := newFlow(passwordlessEnabled, t) + r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + f.Refresh = true + require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f)) + toSnapshot(t, f) + }) + + t.Run("case=passwordless enabled and user has passwordless credentials", func(t *testing.T) { + id, _ := createIdentityAndReturnIdentifier(t, ctx, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`)) + r, f := newFlow(passwordlessEnabled, t) + r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + f.Refresh = true + require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f)) + toSnapshot(t, f) + }) + + t.Run("case=mfa enabled and user has mfa credentials", func(t *testing.T) { + id := createIdentity(t, ctx, reg) + r, f := newFlow(mfaEnabled, t) + r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + f.Refresh = true + f.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f)) + toSnapshot(t, f) + }) + + t.Run("case=mfa enabled but user has passwordless credentials", func(t *testing.T) { + id, _ := createIdentityAndReturnIdentifier(t, ctx, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`)) + r, f := newFlow(mfaEnabled, t) + r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader() + f.Refresh = true + f.RequestedAAL = identity.AuthenticatorAssuranceLevel2 + require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f)) + toSnapshot(t, f) + }) + }) + + t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) { + t.Run("case=no options", func(t *testing.T) { + t.Run("case=passwordless enabled", func(t *testing.T) { + t.Run("case=account enumeration mitigation disabled", func(t *testing.T) { + r, f := newFlow( + configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false), + t, + ) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=account enumeration mitigation enabled", func(t *testing.T) { + r, f := newFlow( + configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true), + t, + ) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + }) + + t.Run("case=mfa enabled", func(t *testing.T) { + t.Run("case=account enumeration mitigation disabled", func(t *testing.T) { + r, f := newFlow( + configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false), + t, + ) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=account enumeration mitigation enabled", func(t *testing.T) { + r, f := newFlow( + configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true), + t, + ) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + }) + }) + + t.Run("case=WithIdentifier", func(t *testing.T) { + t.Run("case=passwordless enabled", func(t *testing.T) { + t.Run("case=account enumeration mitigation disabled", func(t *testing.T) { + r, f := newFlow( + configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false), + t, + ) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=account enumeration mitigation enabled", func(t *testing.T) { + r, f := newFlow( + configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true), + t, + ) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + }) + + t.Run("case=mfa enabled", func(t *testing.T) { + t.Run("case=account enumeration mitigation disabled", func(t *testing.T) { + r, f := newFlow( + configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false), + t, + ) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=account enumeration mitigation enabled", func(t *testing.T) { + r, f := newFlow( + configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true), + t, + ) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + }) + }) + + t.Run("case=WithIdentityHint", func(t *testing.T) { + t.Run("case=account enumeration mitigation enabled", func(t *testing.T) { + mfaEnabled := configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true) + passwordlessEnabled := configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true) + + id := identity.NewIdentity("test-provider") + t.Run("case=passwordless enabled", func(t *testing.T) { + r, f := newFlow(passwordlessEnabled, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=mfa enabled", func(t *testing.T) { + r, f := newFlow(mfaEnabled, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + }) + + t.Run("case=account enumeration mitigation disabled", func(t *testing.T) { + mfaEnabled := configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false) + passwordlessEnabled := configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false) + + id, _ := createIdentityAndReturnIdentifier(t, ctx, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`)) + + t.Run("case=identity has webauthn", func(t *testing.T) { + t.Run("case=passwordless enabled", func(t *testing.T) { + r, f := newFlow(passwordlessEnabled, t) + require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id))) + toSnapshot(t, f) + }) + + t.Run("case=mfa enabled", func(t *testing.T) { + r, f := newFlow(mfaEnabled, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + }) + + t.Run("case=identity does not have a webauthn", func(t *testing.T) { + t.Run("case=passwordless enabled", func(t *testing.T) { + id := identity.NewIdentity("default") + r, f := newFlow(passwordlessEnabled, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + + t.Run("case=mfa enabled", func(t *testing.T) { + id := identity.NewIdentity("default") + r, f := newFlow(mfaEnabled, t) + require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound) + toSnapshot(t, f) + }) + }) + }) + }) + }) + + t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) { + t.Run("case=passwordless enabled", func(t *testing.T) { + r, f := newFlow(passwordlessEnabled, t) + require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f)) + toSnapshot(t, f) + }) + + t.Run("case=mfa enabled", func(t *testing.T) { + r, f := newFlow(mfaEnabled, t) + require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f)) + toSnapshot(t, f) + }) + }) +} diff --git a/selfservice/strategy/webauthn/nodes.go b/selfservice/strategy/webauthn/nodes.go deleted file mode 100644 index cfbffa4be428..000000000000 --- a/selfservice/strategy/webauthn/nodes.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package webauthn - -import ( - "crypto/sha512" - _ "embed" - "encoding/base64" - "fmt" - - "github.com/ory/x/stringsx" - - "github.com/ory/kratos/identity" - "github.com/ory/kratos/text" - "github.com/ory/kratos/ui/node" -) - -func NewWebAuthnConnectionTrigger(options string) *node.Node { - return node.NewInputField(node.WebAuthnRegisterTrigger, "", node.WebAuthnGroup, - node.InputAttributeTypeButton, node.WithInputAttributes(func(a *node.InputAttributes) { - a.OnClick = "window.__oryWebAuthnRegistration(" + options + ")" - })) -} - -func NewWebAuthnScript(src string, contents []byte) *node.Node { - integrity := sha512.Sum512(contents) - return node.NewScriptField(node.WebAuthnScript, src, node.WebAuthnGroup, fmt.Sprintf("sha512-%s", base64.StdEncoding.EncodeToString(integrity[:]))) -} - -func NewWebAuthnConnectionInput() *node.Node { - return node.NewInputField(node.WebAuthnRegister, "", node.WebAuthnGroup, - node.InputAttributeTypeHidden) -} - -func NewWebAuthnLoginTrigger(options string) *node.Node { - return node.NewInputField(node.WebAuthnLoginTrigger, "", node.WebAuthnGroup, - node.InputAttributeTypeButton, node.WithInputAttributes(func(a *node.InputAttributes) { - a.OnClick = "window.__oryWebAuthnLogin(" + options + ")" - })) -} - -func NewWebAuthnLoginInput() *node.Node { - return node.NewInputField(node.WebAuthnLogin, "", node.WebAuthnGroup, - node.InputAttributeTypeHidden) -} - -func NewWebAuthnConnectionName() *node.Node { - return node.NewInputField(node.WebAuthnRegisterDisplayName, "", node.WebAuthnGroup, node.InputAttributeTypeText). - WithMetaLabel(text.NewInfoSelfServiceRegisterWebAuthnDisplayName()) -} - -func NewWebAuthnUnlink(c *identity.CredentialWebAuthn) *node.Node { - return node.NewInputField(node.WebAuthnRemove, fmt.Sprintf("%x", c.ID), node.WebAuthnGroup, - node.InputAttributeTypeSubmit). - WithMetaLabel(text.NewInfoSelfServiceRemoveWebAuthn(stringsx.Coalesce(c.DisplayName, "unnamed"), c.AddedAt)) -} diff --git a/selfservice/strategy/webauthn/registration.go b/selfservice/strategy/webauthn/registration.go index 565125319634..b97613b161cb 100644 --- a/selfservice/strategy/webauthn/registration.go +++ b/selfservice/strategy/webauthn/registration.go @@ -7,7 +7,10 @@ import ( "encoding/json" "net/http" "strings" - "time" + + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/otelx" "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" @@ -23,7 +26,7 @@ import ( "github.com/ory/kratos/ui/container" "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" - "github.com/ory/x/urlx" + "github.com/ory/kratos/x/webauthnx" ) // Update Registration Flow with WebAuthn Method @@ -64,22 +67,21 @@ type updateRegistrationFlowWithWebAuthnMethod struct { // Transient data to pass along to any webhooks // // required: false - TransientPayload json.RawMessage `json:"transient_payload,omitempty"` + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (s *Strategy) RegisterRegistrationRoutes(_ *x.RouterPublic) { } -func (s *Strategy) handleRegistrationError(_ http.ResponseWriter, r *http.Request, f *registration.Flow, p *updateRegistrationFlowWithWebAuthnMethod, err error) error { +func (s *Strategy) handleRegistrationError(r *http.Request, f *registration.Flow, p updateRegistrationFlowWithWebAuthnMethod, err error) error { if f != nil { - if p != nil { - for _, n := range container.NewFromJSON("", node.DefaultGroup, p.Traits, "traits").Nodes { - // we only set the value and not the whole field because we want to keep types from the initial form generation - f.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue()) - } + for _, n := range container.NewFromJSON("", node.DefaultGroup, p.Traits, "traits").Nodes { + // we only set the value and not the whole field because we want to keep types from the initial form generation + f.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue()) } f.UI.Nodes.SetValueAttribute(node.WebAuthnRegisterDisplayName, p.RegisterDisplayName) + if f.Type == flow.TypeBrowser { f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) } @@ -92,29 +94,34 @@ func (s *Strategy) decode(p *updateRegistrationFlowWithWebAuthnMethod, r *http.R return registration.DecodeBody(p, r, s.hd, s.d.Config(), registrationSchema) } -func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registration.Flow, i *identity.Identity) (err error) { - if f.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(r.Context()) { +func (s *Strategy) Register(_ http.ResponseWriter, r *http.Request, regFlow *registration.Flow, i *identity.Identity) (err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.webauthn.strategy.Register") + defer otelx.End(span, &err) + + if regFlow.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(ctx) { + span.SetAttributes(attribute.String("not_responsible_reason", "registration flow is not a browser flow or WebAuthn is not enabled")) return flow.ErrStrategyNotResponsible } var p updateRegistrationFlowWithWebAuthnMethod if err := s.decode(&p, r); err != nil { - return s.handleRegistrationError(w, r, f, &p, err) + return s.handleRegistrationError(r, regFlow, p, err) } - f.TransientPayload = p.TransientPayload + regFlow.TransientPayload = p.TransientPayload - if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { - return s.handleRegistrationError(w, r, f, &p, err) + if err := flow.EnsureCSRF(s.d, r, regFlow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + return s.handleRegistrationError(r, regFlow, p, err) } if len(p.Register) == 0 { + span.SetAttributes(attribute.String("not_responsible_reason", "register field is empty")) return flow.ErrStrategyNotResponsible } p.Method = s.SettingsStrategyID() - if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { - return s.handleRegistrationError(w, r, f, &p, err) + if err := flow.MethodEnabledAndAllowed(ctx, regFlow.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + return s.handleRegistrationError(r, regFlow, p, err) } if len(p.Traits) == 0 { @@ -122,76 +129,81 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat } i.Traits = identity.Traits(p.Traits) - webAuthnSession := gjson.GetBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) + webAuthnSession := gjson.GetBytes(regFlow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) if !webAuthnSession.IsObject() { - return s.handleRegistrationError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object."))) + return s.handleRegistrationError(r, regFlow, p, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object."))) } var webAuthnSess webauthn.SessionData - if err := json.Unmarshal([]byte(gjson.GetBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)).Raw), &webAuthnSess); err != nil { - return s.handleRegistrationError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object but got: %s", err))) + if err := json.Unmarshal([]byte(webAuthnSession.Raw), &webAuthnSess); err != nil { + return s.handleRegistrationError(r, regFlow, p, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object but got: %s", err))) } webAuthnResponse, err := protocol.ParseCredentialCreationResponseBody(strings.NewReader(p.Register)) if err != nil { - return s.handleRegistrationError(w, r, f, &p, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to parse WebAuthn response: %s", err))) + return s.handleRegistrationError(r, regFlow, p, errors.WithStack( + herodot.ErrBadRequest.WithReasonf("Unable to parse WebAuthn response: %s", err))) } - web, err := webauthn.New(s.d.Config().WebAuthnConfig(r.Context())) + web, err := webauthn.New(s.d.Config().WebAuthnConfig(ctx)) if err != nil { - return s.handleRegistrationError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error()))) + return s.handleRegistrationError(r, regFlow, p, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error()))) } - credential, err := web.CreateCredential(NewUser(webAuthnSess.UserID, nil, web.Config), webAuthnSess, webAuthnResponse) + credential, err := web.CreateCredential(webauthnx.NewUser(webAuthnSess.UserID, nil, web.Config), webAuthnSess, webAuthnResponse) if err != nil { if devErr := new(protocol.Error); errors.As(err, &devErr) { s.d.Logger().WithError(err).WithField("error_devinfo", devErr.DevInfo).Error("Failed to create WebAuthn credential") } - return s.handleRegistrationError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to create WebAuthn credential: %s", err))) + return s.handleRegistrationError(r, regFlow, p, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Unable to create WebAuthn credential: %s", err))) } - var cc identity.CredentialsWebAuthnConfig - wc := identity.CredentialFromWebAuthn(credential, true) - wc.AddedAt = time.Now().UTC().Round(time.Second) - wc.DisplayName = p.RegisterDisplayName - wc.IsPasswordless = s.d.Config().WebAuthnForPasswordless(r.Context()) - cc.UserHandle = webAuthnSess.UserID - - cc.Credentials = append(cc.Credentials, *wc) - co, err := json.Marshal(cc) + credentialWebAuthn := identity.CredentialFromWebAuthn(credential, true) + credentialWebAuthn.DisplayName = p.RegisterDisplayName + credentialWebAuthnConfig, err := json.Marshal(identity.CredentialsWebAuthnConfig{ + Credentials: identity.CredentialsWebAuthn{*credentialWebAuthn}, + UserHandle: webAuthnSess.UserID, + }) if err != nil { - return s.handleRegistrationError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error()))) + return s.handleRegistrationError(r, regFlow, p, errors.WithStack( + herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error()))) } - i.UpsertCredentialsConfig(s.ID(), co, 1) - if err := s.validateCredentials(r.Context(), i); err != nil { - return s.handleRegistrationError(w, r, f, &p, err) + i.UpsertCredentialsConfig(s.ID(), credentialWebAuthnConfig, 1) + if err := s.validateCredentials(ctx, i); err != nil { + return s.handleRegistrationError(r, regFlow, p, err) } // Remove the WebAuthn URL from the internal context now that it is set! - f.InternalContext, err = sjson.DeleteBytes(f.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) + regFlow.InternalContext, err = sjson.DeleteBytes(regFlow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) if err != nil { - return s.handleRegistrationError(w, r, f, &p, err) + return s.handleRegistrationError(r, regFlow, p, err) } - if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(r.Context(), f); err != nil { - return s.handleRegistrationError(w, r, f, &p, err) + if err := s.d.RegistrationFlowPersister().UpdateRegistrationFlow(ctx, regFlow); err != nil { + return s.handleRegistrationError(r, regFlow, p, err) } return nil } func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.Flow) error { - if f.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(r.Context()) { + ctx := r.Context() + + if f.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(ctx) { return nil } - ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context()) + ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(ctx) if err != nil { return err } - nodes, err := container.NodesFromJSONSchema(r.Context(), node.DefaultGroup, ds.String(), "", nil) + nodes, err := container.NodesFromJSONSchema(ctx, node.DefaultGroup, ds.String(), "", nil) if err != nil { return err } @@ -200,13 +212,13 @@ func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.F f.UI.SetNode(n) } - web, err := webauthn.New(s.d.Config().WebAuthnConfig(r.Context())) + web, err := webauthn.New(s.d.Config().WebAuthnConfig(ctx)) if err != nil { return errors.WithStack(err) } webauthID := x.NewUUID() - user := NewUser(webauthID[:], nil, s.d.Config().WebAuthnConfig(r.Context())) + user := webauthnx.NewUser(webauthID[:], nil, s.d.Config().WebAuthnConfig(ctx)) option, sessionData, err := web.BeginRegistration(user) if err != nil { return errors.WithStack(err) @@ -222,10 +234,10 @@ func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.F return errors.WithStack(err) } - f.UI.Nodes.Upsert(NewWebAuthnScript(urlx.AppendPaths(s.d.Config().SelfPublicURL(r.Context()), webAuthnRoute).String(), jsOnLoad)) - f.UI.Nodes.Upsert(NewWebAuthnConnectionName()) - f.UI.Nodes.Upsert(NewWebAuthnConnectionInput()) - f.UI.Nodes.Upsert(NewWebAuthnConnectionTrigger(string(injectWebAuthnOptions)). + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx))) + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnConnectionName()) + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnConnectionInput()) + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnConnectionTrigger(string(injectWebAuthnOptions)). WithMetaLabel(text.NewInfoSelfServiceRegistrationRegisterWebAuthn())) f.UI.SetCSRF(s.d.GenerateCSRFToken(r)) diff --git a/selfservice/strategy/webauthn/registration_test.go b/selfservice/strategy/webauthn/registration_test.go index cc20d431230c..8dd3e38bd036 100644 --- a/selfservice/strategy/webauthn/registration_test.go +++ b/selfservice/strategy/webauthn/registration_test.go @@ -40,6 +40,8 @@ var ( registrationFixtureSuccessResponse []byte //go:embed fixtures/registration/success/internal_context.json registrationFixtureSuccessInternalContext []byte + //go:embed fixtures/registration/failure/internal_context_wrong_user_id.json + registrationFixtureFailureInternalContextWrongUserID []byte ) func flowToIsSPA(flow string) bool { @@ -52,6 +54,8 @@ func newRegistrationRegistry(t *testing.T) *driver.RegistryDefault { enableWebAuthn(conf) conf.MustSet(ctx, config.ViperKeyWebAuthnPasswordless, true) conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationLoginHints, true) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, true) + return reg } @@ -141,6 +145,7 @@ func TestRegistration(t *testing.T) { testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ "2.attributes.value", "5.attributes.onclick", + "5.attributes.value", "6.attributes.nonce", "6.attributes.src", }) @@ -236,6 +241,47 @@ func TestRegistration(t *testing.T) { return body, res, f } + t.Run("case=should return an error because internal context is invalid", func(t *testing.T) { + email := testhelpers.RandomEmail() + + for _, tc := range []struct { + name string + internalContext string + }{{ + name: "invalid json", + internalContext: "invalid", + }, { + name: "wrong user ID", + internalContext: string(registrationFixtureFailureInternalContextWrongUserID), + }} { + tc := tc + t.Run("context="+tc.name, func(t *testing.T) { + var values = func(v url.Values) { + v.Set("traits.username", email) + v.Set("traits.foobar", "bazbar") + v.Set(node.WebAuthnRegister, string(registrationFixtureSuccessResponse)) + v.Del("method") + } + + for _, f := range flows { + t.Run("type="+f, func(t *testing.T) { + actual, _, _ := submitWebAuthnRegistrationWithClient(t, f, + []byte(tc.internalContext), + testhelpers.NewClientWithCookies(t), + values, + ) + + if f == "spa" { + assert.Equal(t, "Internal Server Error", gjson.Get(actual, "error.status").String(), "%s", actual) + } else { + assert.Equal(t, "Internal Server Error", gjson.Get(actual, "status").String(), "%s", actual) + } + }) + } + }) + } + }) + t.Run("case=should fail to create identity if schema is missing the identifier", func(t *testing.T) { testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/noid.schema.json") t.Cleanup(func() { @@ -322,6 +368,13 @@ func TestRegistration(t *testing.T) { i, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeWebAuthn, email) require.NoError(t, err) assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual) + + if f == "spa" { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual) + assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), redirNoSessionTS.URL+"/registration-return-ts", "%s", actual) + } else { + assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual) + } }) } }) @@ -393,7 +446,7 @@ func TestRegistration(t *testing.T) { actual, _, _ = makeRegistration(t, f, values(email)) assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+registration.RouteSubmitFlow, "%s", actual) registrationhelpers.CheckFormContent(t, []byte(actual), node.WebAuthnRegisterTrigger, "csrf_token", "traits.username") - assert.Equal(t, "You tried signing in with "+email+" which is already in use by another account. You can sign in using your password.", gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) + assert.Equal(t, "You tried signing in with "+email+" which is already in use by another account. You can sign in using your passkey or a security key.", gjson.Get(actual, "ui.messages.0.text").String(), "%s", actual) }) } }) diff --git a/selfservice/strategy/webauthn/settings.go b/selfservice/strategy/webauthn/settings.go index 51fbcf0f5adb..126e147f2979 100644 --- a/selfservice/strategy/webauthn/settings.go +++ b/selfservice/strategy/webauthn/settings.go @@ -4,15 +4,20 @@ package webauthn import ( + "context" "encoding/json" "fmt" "net/http" "strings" "time" - "github.com/ory/kratos/text" + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/otelx" - "github.com/ory/x/urlx" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x/webauthnx" "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" @@ -83,6 +88,11 @@ type updateSettingsFlowWithWebAuthnMethod struct { // // swagger:ignore Flow string `json:"flow"` + + // Transient data to pass along to any webhooks + // + // required: false + TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"` } func (p *updateSettingsFlowWithWebAuthnMethod) GetFlowID() uuid.UUID { @@ -93,36 +103,41 @@ func (p *updateSettingsFlowWithWebAuthnMethod) SetFlowID(rid uuid.UUID) { p.Flow = rid.String() } -func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (*settings.UpdateContext, error) { +func (s *Strategy) Settings(w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (_ *settings.UpdateContext, err error) { + ctx, span := s.d.Tracer(r.Context()).Tracer().Start(r.Context(), "selfservice.strategy.webauthn.strategy.Settings") + defer otelx.End(span, &err) + if f.Type != flow.TypeBrowser { + span.SetAttributes(attribute.String("not_responsible_reason", "flow is not a browser flow")) return nil, flow.ErrStrategyNotResponsible } var p updateSettingsFlowWithWebAuthnMethod ctxUpdate, err := settings.PrepareUpdate(s.d, w, r, f, ss, settings.ContinuityKey(s.SettingsStrategyID()), &p) if errors.Is(err, settings.ErrContinuePreviousAction) { - return ctxUpdate, s.continueSettingsFlow(w, r, ctxUpdate, &p) + return ctxUpdate, s.continueSettingsFlow(ctx, w, r, ctxUpdate, p) } else if err != nil { - return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, p, err) } if err := s.decodeSettingsFlow(r, &p); err != nil { - return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, p, err) } - if len(p.Register+p.Remove) > 0 { + if len(p.Register)+len(p.Remove) > 0 { // This method has only two submit buttons p.Method = s.SettingsStrategyID() - if err := flow.MethodEnabledAndAllowed(r.Context(), f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { - return nil, s.handleSettingsError(w, r, ctxUpdate, &p, err) + if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.SettingsStrategyID(), p.Method, s.d); err != nil { + return nil, s.handleSettingsError(w, r, ctxUpdate, p, err) } } else { + span.SetAttributes(attribute.String("not_responsible_reason", "neither register nor remove is set")) return nil, errors.WithStack(flow.ErrStrategyNotResponsible) } // This does not come from the payload! p.Flow = ctxUpdate.Flow.ID.String() - if err := s.continueSettingsFlow(w, r, ctxUpdate, &p); err != nil { - return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, &p, err) + if err := s.continueSettingsFlow(ctx, w, r, ctxUpdate, p); err != nil { + return ctxUpdate, s.handleSettingsError(w, r, ctxUpdate, p, err) } return ctxUpdate, nil @@ -135,6 +150,7 @@ func (s *Strategy) decodeSettingsFlow(r *http.Request, dest interface{}) error { } return decoderx.NewHTTP().Decode(r, dest, compiler, + decoderx.HTTPKeepRequestBody(true), decoderx.HTTPDecoderAllowedMethods("POST", "GET"), decoderx.HTTPDecoderSetValidatePayloads(true), decoderx.HTTPDecoderJSONFollowsFormFormat(), @@ -142,19 +158,20 @@ func (s *Strategy) decodeSettingsFlow(r *http.Request, dest interface{}) error { } func (s *Strategy) continueSettingsFlow( + ctx context.Context, w http.ResponseWriter, r *http.Request, - ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithWebAuthnMethod, + ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithWebAuthnMethod, ) error { if len(p.Register+p.Remove) > 0 { - if err := flow.MethodEnabledAndAllowed(r.Context(), flow.SettingsFlow, s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { + if err := flow.MethodEnabledAndAllowed(ctx, flow.SettingsFlow, s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { return err } - if err := flow.EnsureCSRF(s.d, r, ctxUpdate.Flow.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { + if err := flow.EnsureCSRF(s.d, r, ctxUpdate.Flow.Type, s.d.Config().DisableAPIFlowEnforcement(ctx), s.d.GenerateCSRFToken, p.CSRFToken); err != nil { return err } - if ctxUpdate.Session.AuthenticatedAt.Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(r.Context())).Before(time.Now()) { + if ctxUpdate.Session.AuthenticatedAt.Add(s.d.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(ctx)).Before(time.Now()) { return errors.WithStack(settings.NewFlowNeedsReAuth()) } } else { @@ -162,16 +179,16 @@ func (s *Strategy) continueSettingsFlow( } if len(p.Register) > 0 { - return s.continueSettingsFlowAdd(w, r, ctxUpdate, p) + return s.continueSettingsFlowAdd(ctx, ctxUpdate, p) } else if len(p.Remove) > 0 { - return s.continueSettingsFlowRemove(w, r, ctxUpdate, p) + return s.continueSettingsFlowRemove(ctx, w, r, ctxUpdate, p) } return errors.New("ended up in unexpected state") } -func (s *Strategy) continueSettingsFlowRemove(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithWebAuthnMethod) error { - i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), ctxUpdate.Session.IdentityID) +func (s *Strategy) continueSettingsFlowRemove(ctx context.Context, w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithWebAuthnMethod) error { + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ctxUpdate.Session.IdentityID) if err != nil { return err } @@ -200,13 +217,13 @@ func (s *Strategy) continueSettingsFlowRemove(w http.ResponseWriter, r *http.Req return errors.WithStack(herodot.ErrBadRequest.WithReasonf("You tried to remove a WebAuthn credential which does not exist.")) } - count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(r.Context(), i) + count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(ctx, i) if err != nil { return err } if count < 2 && wasPasswordless { - return s.handleSettingsError(w, r, ctxUpdate, p, errors.WithStack(ErrNotEnoughCredentials)) + return s.handleSettingsError(w, r, ctxUpdate, p, errors.WithStack(webauthnx.ErrNotEnoughCredentials)) } if len(updated) == 0 { @@ -226,7 +243,7 @@ func (s *Strategy) continueSettingsFlowRemove(w http.ResponseWriter, r *http.Req return nil } -func (s *Strategy) continueSettingsFlowAdd(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithWebAuthnMethod) error { +func (s *Strategy) continueSettingsFlowAdd(ctx context.Context, ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithWebAuthnMethod) error { webAuthnSession := gjson.GetBytes(ctxUpdate.Flow.InternalContext, flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData)) if !webAuthnSession.IsObject() { return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected WebAuthN in internal context to be an object.")) @@ -242,17 +259,17 @@ func (s *Strategy) continueSettingsFlowAdd(w http.ResponseWriter, r *http.Reques return errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to parse WebAuthn response: %s", err)) } - web, err := webauthn.New(s.d.Config().WebAuthnConfig(r.Context())) + web, err := webauthn.New(s.d.Config().WebAuthnConfig(ctx)) if err != nil { return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to get webAuthn config.").WithDebug(err.Error())) } - credential, err := web.CreateCredential(NewUser(ctxUpdate.Session.IdentityID[:], nil, web.Config), webAuthnSess, webAuthnResponse) + credential, err := web.CreateCredential(webauthnx.NewUser(ctxUpdate.Session.IdentityID[:], nil, web.Config), webAuthnSess, webAuthnResponse) if err != nil { return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to create WebAuthn credential: %s", err)) } - i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), ctxUpdate.Session.IdentityID) + i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ctxUpdate.Session.IdentityID) if err != nil { return err } @@ -264,10 +281,10 @@ func (s *Strategy) continueSettingsFlowAdd(w http.ResponseWriter, r *http.Reques return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error())) } - wc := identity.CredentialFromWebAuthn(credential, s.d.Config().WebAuthnForPasswordless(r.Context())) + wc := identity.CredentialFromWebAuthn(credential, s.d.Config().WebAuthnForPasswordless(ctx)) wc.AddedAt = time.Now().UTC().Round(time.Second) wc.DisplayName = p.RegisterDisplayName - wc.IsPasswordless = s.d.Config().WebAuthnForPasswordless(r.Context()) + wc.IsPasswordless = s.d.Config().WebAuthnForPasswordless(ctx) cc.UserHandle = ctxUpdate.Session.IdentityID[:] cc.Credentials = append(cc.Credentials, *wc) @@ -277,7 +294,7 @@ func (s *Strategy) continueSettingsFlowAdd(w http.ResponseWriter, r *http.Reques } i.UpsertCredentialsConfig(s.ID(), co, 1) - if err := s.validateCredentials(r.Context(), i); err != nil { + if err := s.validateCredentials(ctx, i); err != nil { return err } @@ -287,17 +304,17 @@ func (s *Strategy) continueSettingsFlowAdd(w http.ResponseWriter, r *http.Reques return err } - if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(r.Context(), ctxUpdate.Flow); err != nil { + if err := s.d.SettingsFlowPersister().UpdateSettingsFlow(ctx, ctxUpdate.Flow); err != nil { return err } aal := identity.AuthenticatorAssuranceLevel1 - if !s.d.Config().WebAuthnForPasswordless(r.Context()) { + if !s.d.Config().WebAuthnForPasswordless(ctx) { aal = identity.AuthenticatorAssuranceLevel2 } // Since we added the method, it also means that we have authenticated it - if err := s.d.SessionManager().SessionAddAuthenticationMethods(r.Context(), ctxUpdate.Session.ID, session.AuthenticationMethod{ + if err := s.d.SessionManager().SessionAddAuthenticationMethods(ctx, ctxUpdate.Session.ID, session.AuthenticationMethod{ Method: s.ID(), AAL: aal, }); err != nil { @@ -348,11 +365,10 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity // We only show the option to remove a credential, if it is not the last one when passwordless, // or, if it is for MFA we show it always. cred := &webAuthns.Credentials[k] - if cred.IsPasswordless && count < 2 { + f.UI.Nodes.Append(webauthnx.NewWebAuthnUnlink(cred, func(a *node.InputAttributes) { // Do not remove this node because it is the last credential the identity can sign in with. - continue - } - f.UI.Nodes.Append(NewWebAuthnUnlink(cred)) + a.Disabled = cred.IsPasswordless && count < 2 + })) } } @@ -361,7 +377,7 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity return errors.WithStack(err) } - option, sessionData, err := web.BeginRegistration(NewUser(id.ID.Bytes(), nil, web.Config)) + option, sessionData, err := web.BeginRegistration(webauthnx.NewUser(id.ID.Bytes(), nil, web.Config)) if err != nil { return errors.WithStack(err) } @@ -376,15 +392,15 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity return errors.WithStack(err) } - f.UI.Nodes.Upsert(NewWebAuthnScript(urlx.AppendPaths(s.d.Config().SelfPublicURL(r.Context()), webAuthnRoute).String(), jsOnLoad)) - f.UI.Nodes.Upsert(NewWebAuthnConnectionName()) - f.UI.Nodes.Upsert(NewWebAuthnConnectionTrigger(string(injectWebAuthnOptions)). + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(r.Context()))) + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnConnectionName()) + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnConnectionTrigger(string(injectWebAuthnOptions)). WithMetaLabel(text.NewInfoSelfServiceSettingsRegisterWebAuthn())) - f.UI.Nodes.Upsert(NewWebAuthnConnectionInput()) + f.UI.Nodes.Upsert(webauthnx.NewWebAuthnConnectionInput()) return nil } -func (s *Strategy) handleSettingsError(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithWebAuthnMethod, err error) error { +func (s *Strategy) handleSettingsError(w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p updateSettingsFlowWithWebAuthnMethod, err error) error { // Do not pause flow if the flow type is an API flow as we can't save cookies in those flows. if e := new(settings.FlowNeedsReAuth); errors.As(err, &e) && ctxUpdate.Flow != nil && ctxUpdate.Flow.Type == flow.TypeBrowser { if err := s.d.ContinuityManager().Pause(r.Context(), w, r, settings.ContinuityKey(s.SettingsStrategyID()), settings.ContinuityOptions(p, ctxUpdate.GetSessionIdentity())...); err != nil { diff --git a/selfservice/strategy/webauthn/settings_test.go b/selfservice/strategy/webauthn/settings_test.go index 27413df38b16..bf37258d5706 100644 --- a/selfservice/strategy/webauthn/settings_test.go +++ b/selfservice/strategy/webauthn/settings_test.go @@ -53,19 +53,21 @@ var settingsFixtureSuccessInternalContext []byte const registerDisplayNameGJSONQuery = "ui.nodes.#(attributes.name==" + node.WebAuthnRegisterDisplayName + ")" func createIdentityWithoutWebAuthn(t *testing.T, reg driver.Registry) *identity.Identity { - id := createIdentity(t, reg) + id := createIdentity(t, ctx, reg) delete(id.Credentials, identity.CredentialsTypeWebAuthn) require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), id)) return id } -func createIdentityAndReturnIdentifier(t *testing.T, reg driver.Registry, conf []byte) (*identity.Identity, string) { +func createIdentityAndReturnIdentifier(t *testing.T, ctx context.Context, reg driver.Registry, conf []byte) (*identity.Identity, string) { identifier := x.NewUUID().String() + "@ory.sh" password := x.NewUUID().String() - p, err := reg.Hasher(ctx).Generate(context.Background(), []byte(password)) + p, err := reg.Hasher(ctx).Generate(ctx, []byte(password)) require.NoError(t, err) i := &identity.Identity{ - Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)), + SchemaID: "default", + NID: uuid.Must(uuid.NewV4()), + Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)), VerifiableAddresses: []identity.VerifiableAddress{ { Value: identifier, @@ -77,7 +79,7 @@ func createIdentityAndReturnIdentifier(t *testing.T, reg driver.Registry, conf [ if conf == nil { conf = []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo"},{"id":"YmFyYmFy","display_name":"bar"}]}`) } - require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i)) i.Credentials = map[identity.CredentialsType]identity.Credentials{ identity.CredentialsTypePassword: { Type: identity.CredentialsTypePassword, @@ -90,20 +92,21 @@ func createIdentityAndReturnIdentifier(t *testing.T, reg driver.Registry, conf [ Config: conf, }, } - require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), i)) + require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(ctx, i)) return i, identifier } -func createIdentity(t *testing.T, reg driver.Registry) *identity.Identity { - id, _ := createIdentityAndReturnIdentifier(t, reg, nil) +func createIdentity(t *testing.T, ctx context.Context, reg driver.Registry) *identity.Identity { + id, _ := createIdentityAndReturnIdentifier(t, ctx, reg, nil) return id } func enableWebAuthn(conf *config.Config) { - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".enabled", true) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.display_name", "Ory Corp") - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.id", "localhost") - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.origin", "http://localhost:4455") + webauthn := config.ViperKeySelfServiceStrategyConfig + "." + string(identity.CredentialsTypeWebAuthn) + conf.MustSet(ctx, webauthn+".enabled", true) + conf.MustSet(ctx, webauthn+".config.rp.display_name", "Ory Corp") + conf.MustSet(ctx, webauthn+".config.rp.id", "localhost") + conf.MustSet(ctx, webauthn+".config.rp.origin", "http://localhost:4455") } func ensureReplacement(t *testing.T, index string, ui kratos.UiContainer, expected string) { @@ -135,48 +138,50 @@ func TestCompleteSettings(t *testing.T) { conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"}) t.Run("case=a device is shown which can be unlinked", func(t *testing.T) { - id := createIdentity(t, reg) + id := createIdentity(t, ctx, reg) - apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, publicTS) testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ "0.attributes.value", - "4.attributes.onclick", + "5.attributes.onclick", + "5.attributes.value", "6.attributes.src", "6.attributes.nonce", }) - ensureReplacement(t, "4", f.Ui, "Ory Corp") + ensureReplacement(t, "5", f.Ui, "Ory Corp") }) t.Run("case=one activation element is shown", func(t *testing.T) { id := createIdentityWithoutWebAuthn(t, reg) require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), id)) - apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, publicTS) testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{ "0.attributes.value", - "2.attributes.onload", - "2.attributes.onclick", + "3.attributes.onload", + "3.attributes.onclick", + "3.attributes.value", "4.attributes.src", "4.attributes.nonce", }) - ensureReplacement(t, "2", f.Ui, "Ory Corp") + ensureReplacement(t, "3", f.Ui, "Ory Corp") }) t.Run("case=webauthn only works for browsers", func(t *testing.T) { id := createIdentityWithoutWebAuthn(t, reg) require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), id)) - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) assert.Empty(t, f.Ui.Nodes) }) doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) { - apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id) + apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS) values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) v(values) @@ -185,7 +190,7 @@ func TestCompleteSettings(t *testing.T) { } doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) { - browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id) f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS) values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes) v(values) @@ -312,8 +317,9 @@ func TestCompleteSettings(t *testing.T) { // We load our identity which we will use to replay the webauth session var id identity.Identity require.NoError(t, json.Unmarshal(settingsFixtureSuccessIdentity, &id)) + id.NID = uuid.Must(uuid.NewV4()) _ = reg.PrivilegedIdentityPool().DeleteIdentity(context.Background(), id.ID) - browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, &id) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, &id) f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS) // We inject the session to replay @@ -328,6 +334,7 @@ func TestCompleteSettings(t *testing.T) { values.Set(node.WebAuthnRegister, string(settingsFixtureSuccessResponse)) values.Set(node.WebAuthnRegisterDisplayName, "foobar") body, res := testhelpers.SettingsMakeRequest(t, false, spa, f, browserClient, testhelpers.EncodeFormAsJSON(t, spa, values)) + require.Equal(t, http.StatusOK, res.StatusCode, body) if spa { assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow) @@ -339,7 +346,7 @@ func TestCompleteSettings(t *testing.T) { actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID) require.NoError(t, err) cred, ok := actual.GetCredentials(identity.CredentialsTypeWebAuthn) - assert.True(t, ok) + require.True(t, ok) assert.Len(t, gjson.GetBytes(cred.Config, "credentials").Array(), 1) actualFlow, err := reg.SettingsFlowPersister().GetSettingsFlow(context.Background(), uuid.FromStringOrNil(f.Id)) @@ -366,7 +373,7 @@ func TestCompleteSettings(t *testing.T) { }) run := func(t *testing.T, spa bool) { - id := createIdentity(t, reg) + id := createIdentity(t, ctx, reg) id.DeleteCredentialsType(identity.CredentialsTypePassword) conf := sqlxx.JSONRawMessage(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`) id.UpsertCredentialsConfig(identity.CredentialsTypeWebAuthn, conf, 0) @@ -374,7 +381,7 @@ func TestCompleteSettings(t *testing.T) { body, res := doBrowserFlow(t, spa, func(v url.Values) { // The remove key should be empty - snapshotx.SnapshotTExcept(t, v, []string{"csrf_token"}) + snapshotx.SnapshotTExcept(t, v, []string{"csrf_token", "webauthn_register_trigger"}) v.Set(node.WebAuthnRemove, "666f6f666f6f") }, id) @@ -408,14 +415,17 @@ func TestCompleteSettings(t *testing.T) { t.Run("case=possible to remove webauthn credential if it is MFA at all times", func(t *testing.T) { run := func(t *testing.T, spa bool) { - id := createIdentity(t, reg) + id := createIdentity(t, ctx, reg) id.DeleteCredentialsType(identity.CredentialsTypePassword) id.UpsertCredentialsConfig(identity.CredentialsTypeWebAuthn, sqlxx.JSONRawMessage(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":false}]}`), 0) require.NoError(t, reg.IdentityManager().Update(ctx, id, identity.ManagerAllowWriteProtectedTraits)) body, res := doBrowserFlow(t, spa, func(v url.Values) { // The remove key should be set - snapshotx.SnapshotTExcept(t, v, []string{"csrf_token"}) + snapshotx.SnapshotTExcept(t, v, []string{ + "csrf_token", + "webauthn_register_trigger", + }) v.Set(node.WebAuthnRemove, "666f6f666f6f") }, id) @@ -445,7 +455,7 @@ func TestCompleteSettings(t *testing.T) { t.Run("case=remove all security keys", func(t *testing.T) { run := func(t *testing.T, spa bool) { - id := createIdentity(t, reg) + id := createIdentity(t, ctx, reg) allCred, ok := id.GetCredentials(identity.CredentialsTypeWebAuthn) assert.True(t, ok) @@ -464,6 +474,13 @@ func TestCompleteSettings(t *testing.T) { assert.Contains(t, res.Request.URL.String(), uiTS.URL) } assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body) + + if spa { + assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body) + assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", body) + } else { + assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body) + } } actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID) @@ -486,7 +503,7 @@ func TestCompleteSettings(t *testing.T) { t.Run("case=fails with browser submit register payload is invalid", func(t *testing.T) { run := func(t *testing.T, spa bool) { - id := createIdentity(t, reg) + id := createIdentity(t, ctx, reg) body, res := doBrowserFlow(t, spa, func(v url.Values) { v.Set(node.WebAuthnRemove, fmt.Sprintf("%x", []byte("foofoo"))) }, id) @@ -523,9 +540,10 @@ func TestCompleteSettings(t *testing.T) { isSPA := f == "spa" var id identity.Identity + id.NID = uuid.Must(uuid.NewV4()) require.NoError(t, json.Unmarshal(settingsFixtureSuccessIdentity, &id)) _ = reg.PrivilegedIdentityPool().DeleteIdentity(context.Background(), id.ID) - browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, &id) + browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, &id) f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, isSPA, publicTS) // We inject the session to replay diff --git a/selfservice/strategy/webauthn/strategy.go b/selfservice/strategy/webauthn/strategy.go index 998490055996..82a0b7df9b2a 100644 --- a/selfservice/strategy/webauthn/strategy.go +++ b/selfservice/strategy/webauthn/strategy.go @@ -6,6 +6,7 @@ package webauthn import ( "context" "encoding/json" + "strings" "github.com/pkg/errors" @@ -34,6 +35,7 @@ type webauthnStrategyDependencies interface { x.WriterProvider x.CSRFTokenGeneratorProvider x.CSRFProvider + x.TracingProvider config.Provider @@ -80,26 +82,37 @@ func NewStrategy(d any) *Strategy { } } -func (s *Strategy) CountActiveMultiFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { +func (s *Strategy) CountActiveMultiFactorCredentials(_ context.Context, cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { return s.countCredentials(cc, false) } -func (s *Strategy) CountActiveFirstFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { +func (s *Strategy) CountActiveFirstFactorCredentials(_ context.Context, cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { return s.countCredentials(cc, true) } -func (s *Strategy) countCredentials(cc map[identity.CredentialsType]identity.Credentials, passwordless bool) (count int, err error) { +func (s *Strategy) countCredentials(cc map[identity.CredentialsType]identity.Credentials, onlyPasswordlessCredentials bool) (count int, err error) { for _, c := range cc { - if c.Type == s.ID() && len(c.Config) > 0 && len(c.Identifiers) > 0 { + if c.Type == s.ID() && len(c.Config) > 0 { var conf identity.CredentialsWebAuthnConfig if err = json.Unmarshal(c.Config, &conf); err != nil { return 0, errors.WithStack(err) } - for _, c := range conf.Credentials { - if c.IsPasswordless == passwordless { - count++ + for _, cred := range conf.Credentials { + if cred.IsPasswordless && len(strings.Join(c.Identifiers, "")) == 0 { + // If this is a passwordless credential, it will only work if the identifier is set, as + // we use the identifier to look up the identity. If the identifier is not set, we can + // assume that the user can't sign in using this method. + continue } + + if cred.IsPasswordless != onlyPasswordlessCredentials { + continue + } + + // If the credential is passwordless and we require passwordless credentials, or if the credential is not + // passwordless and we require non-passwordless credentials, we count it. + count++ } } } @@ -114,7 +127,7 @@ func (s *Strategy) NodeGroup() node.UiNodeGroup { return node.WebAuthnGroup } -func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context, _ session.AuthenticationMethods) session.AuthenticationMethod { +func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.AuthenticationMethod { aal := identity.AuthenticatorAssuranceLevel1 if !s.d.Config().WebAuthnForPasswordless(ctx) { aal = identity.AuthenticatorAssuranceLevel2 diff --git a/selfservice/strategy/webauthn/strategy_test.go b/selfservice/strategy/webauthn/strategy_test.go index cc5f6fafb475..5ce70310d9e9 100644 --- a/selfservice/strategy/webauthn/strategy_test.go +++ b/selfservice/strategy/webauthn/strategy_test.go @@ -26,13 +26,13 @@ func TestCompletedAuthenticationMethod(t *testing.T) { assert.Equal(t, session.AuthenticationMethod{ Method: strategy.ID(), AAL: identity.AuthenticatorAssuranceLevel2, - }, strategy.CompletedAuthenticationMethod(context.Background(), session.AuthenticationMethods{})) + }, strategy.CompletedAuthenticationMethod(context.Background())) conf.MustSet(ctx, config.ViperKeyWebAuthnPasswordless, true) assert.Equal(t, session.AuthenticationMethod{ Method: strategy.ID(), AAL: identity.AuthenticatorAssuranceLevel1, - }, strategy.CompletedAuthenticationMethod(context.Background(), session.AuthenticationMethods{})) + }, strategy.CompletedAuthenticationMethod(context.Background())) } func TestCountActiveFirstFactorCredentials(t *testing.T) { @@ -64,6 +64,14 @@ func TestCountActiveFirstFactorCredentials(t *testing.T) { }}, expectedMulti: 1, }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Identifiers: []string{}, // also works without identifier + Config: []byte(`{"credentials": [{}]}`), + }}, + expectedMulti: 1, + }, { in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { Type: strategy.ID(), @@ -72,6 +80,13 @@ func TestCountActiveFirstFactorCredentials(t *testing.T) { }}, expectedFirst: 1, }, + { + in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { + Type: strategy.ID(), + Config: []byte(`{"credentials": [{"is_passwordless": true}]}`), + }}, + expectedFirst: 0, // missing identifier + }, { in: map[identity.CredentialsType]identity.Credentials{strategy.ID(): { Type: strategy.ID(), @@ -105,11 +120,11 @@ func TestCountActiveFirstFactorCredentials(t *testing.T) { cc[c.Type] = c } - actual, err := strategy.CountActiveFirstFactorCredentials(cc) + actual, err := strategy.CountActiveFirstFactorCredentials(ctx, cc) require.NoError(t, err) assert.Equal(t, tc.expectedFirst, actual) - actual, err = strategy.CountActiveMultiFactorCredentials(cc) + actual, err = strategy.CountActiveMultiFactorCredentials(ctx, cc) require.NoError(t, err) assert.Equal(t, tc.expectedMulti, actual) }) diff --git a/selfservice/strategy/webauthn/user.go b/selfservice/strategy/webauthn/user.go deleted file mode 100644 index 823ca93de106..000000000000 --- a/selfservice/strategy/webauthn/user.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package webauthn - -import "github.com/go-webauthn/webauthn/webauthn" - -var _ webauthn.User = (*User)(nil) - -type User struct { - id []byte - c []webauthn.Credential - cfg *webauthn.Config -} - -func NewUser(id []byte, c []webauthn.Credential, cfg *webauthn.Config) *User { - return &User{ - id: id, - c: c, - cfg: cfg, - } -} - -func (u *User) WebAuthnID() []byte { - return u.id -} - -func (u *User) WebAuthnName() string { - return u.cfg.RPDisplayName -} - -func (u *User) WebAuthnDisplayName() string { - return u.cfg.RPDisplayName -} - -func (u *User) WebAuthnIcon() string { - return "" // Icon option has been removed due to security considerations. -} - -func (u *User) WebAuthnCredentials() []webauthn.Credential { - return u.c -} diff --git a/session/handler.go b/session/handler.go index 23c6012dc646..635ebdb621ed 100644 --- a/session/handler.go +++ b/session/handler.go @@ -385,7 +385,12 @@ func (h *Handler) whoami(w http.ResponseWriter, r *http.Request, _ httprouter.Pa // Set Cache header only when configured, and when no tokenization is requested. if c.SessionWhoAmICaching(ctx) && len(tokenizeTemplate) == 0 { - w.Header().Set("Ory-Session-Cache-For", fmt.Sprintf("%d", int64(time.Until(s.ExpiresAt).Seconds()))) + expiry := time.Until(s.ExpiresAt) + if c.SessionWhoAmICachingMaxAge(ctx) > 0 && expiry > c.SessionWhoAmICachingMaxAge(ctx) { + expiry = c.SessionWhoAmICachingMaxAge(ctx) + } + + w.Header().Set("Ory-Session-Cache-For", fmt.Sprintf("%0.f", expiry.Seconds())) } if err := h.r.SessionManager().RefreshCookie(ctx, w, r, s); err != nil { @@ -463,11 +468,19 @@ type listSessionsRequest struct { // If no value is provided, the expandable properties are skipped. // // required: false - // enum: identity,devices // in: query - ExpandOptions []string `json:"expand"` + ExpandOptions []SessionExpandable `json:"expand"` } +// Expandable properties of a session +// swagger:enum SessionExpandable +type SessionExpandable string + +const ( + SessionExpandableIdentity SessionExpandable = "identity" + SessionExpandableDevices SessionExpandable = "devices" +) + // Session List Response // // The response given when listing sessions in an administrative context. @@ -557,9 +570,8 @@ type getSession struct { // If no value is provided, the expandable properties are skipped. // // required: false - // enum: identity,devices // in: query - ExpandOptions []string `json:"expand"` + ExpandOptions []SessionExpandable `json:"expand"` // ID is the session's ID. // @@ -1041,6 +1053,13 @@ type extendSession struct { // Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it // will only extend the session after the specified time has passed. // +// This endpoint returns per default a 204 No Content response on success. Older Ory Network projects may +// return a 200 OK response with the session in the body. Returning the session as part of the response +// will be deprecated in the future and should not be relied upon. +// +// This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those +// scenarios. This endpoint also returns 404 errors if the session does not exist. +// // Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method. // // Schemes: http, https @@ -1050,30 +1069,35 @@ type extendSession struct { // // Responses: // 200: session +// 204: emptyResponse // 400: errorGeneric // 404: errorGeneric // default: errorGeneric func (h *Handler) adminSessionExtend(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - iID, err := uuid.FromString(ps.ByName("id")) + id, err := uuid.FromString(ps.ByName("id")) if err != nil { h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithError(err.Error()).WithDebug("could not parse UUID"))) return } - s, err := h.r.SessionPersister().GetSession(r.Context(), iID, ExpandNothing) - if err != nil { + c := h.r.Config() + if err := h.r.SessionPersister().ExtendSession(r.Context(), id); err != nil { h.r.Writer().WriteError(w, r, err) return } - c := h.r.Config() - if s.CanBeRefreshed(r.Context(), c) { - if err := h.r.SessionPersister().UpsertSession(r.Context(), s.Refresh(r.Context(), c)); err != nil { - h.r.Writer().WriteError(w, r, err) - return - } + // Default behavior going forward. + if c.FeatureFlagFasterSessionExtend(r.Context()) { + w.WriteHeader(http.StatusNoContent) + return } + // WARNING - this will be deprecated at some point! + s, err := h.r.SessionPersister().GetSession(r.Context(), id, ExpandDefault) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } h.r.Writer().Write(w, r, s) } diff --git a/session/handler_test.go b/session/handler_test.go index e0a5d37e403c..91b17282463b 100644 --- a/session/handler_test.go +++ b/session/handler_test.go @@ -166,8 +166,9 @@ func TestSessionWhoAmI(t *testing.T) { }) t.Run("case=http methods", func(t *testing.T) { - run := func(t *testing.T, cacheEnabled bool) { + run := func(t *testing.T, cacheEnabled bool, maxAge time.Duration) { conf.MustSet(ctx, config.ViperKeySessionWhoAmICaching, cacheEnabled) + conf.MustSet(ctx, config.ViperKeySessionWhoAmICachingMaxAge, maxAge) client := testhelpers.NewClientWithCookies(t) // No cookie yet -> 401 @@ -177,6 +178,7 @@ func TestSessionWhoAmI(t *testing.T) { if cacheEnabled { assert.NotEmpty(t, res.Header.Get("Ory-Session-Cache-For")) + assert.Equal(t, "60", res.Header.Get("Ory-Session-Cache-For")) } else { assert.Empty(t, res.Header.Get("Ory-Session-Cache-For")) } @@ -206,7 +208,13 @@ func TestSessionWhoAmI(t *testing.T) { assert.NotEmpty(t, res.Header.Get("X-Kratos-Authenticated-Identity-Id")) if cacheEnabled { - assert.NotEmpty(t, res.Header.Get("Ory-Session-Cache-For")) + var expectedSeconds int + if maxAge > 0 { + expectedSeconds = int(maxAge.Seconds()) + } else { + expectedSeconds = int(conf.SessionLifespan(ctx).Seconds()) + } + assert.InDelta(t, expectedSeconds, x.Must(strconv.Atoi(res.Header.Get("Ory-Session-Cache-For"))), 5) } else { assert.Empty(t, res.Header.Get("Ory-Session-Cache-For")) } @@ -222,11 +230,15 @@ func TestSessionWhoAmI(t *testing.T) { } t.Run("cache disabled", func(t *testing.T) { - run(t, false) + run(t, false, 0) }) t.Run("cache enabled", func(t *testing.T) { - run(t, true) + run(t, true, 0) + }) + + t.Run("cache enabled with max age", func(t *testing.T) { + run(t, true, time.Minute) }) }) @@ -296,7 +308,7 @@ func TestSessionWhoAmI(t *testing.T) { i := identity.Identity{Traits: []byte("{}"), State: identity.StateActive} require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i)) - s, err := session.NewActiveSession(&i, conf, time.Now(), identity.CredentialsTypePassword) + s, err := testhelpers.NewActiveSession(&i, conf, time.Now(), identity.CredentialsTypePassword) require.NoError(t, err) require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), s)) require.NotEmpty(t, s.Token) @@ -603,7 +615,7 @@ func TestHandlerAdminSessionManagement(t *testing.T) { }) t.Run("should redirect to public for whoami", func(t *testing.T) { - client := testhelpers.NewHTTPClientWithSessionToken(t, reg, s) + client := testhelpers.NewHTTPClientWithSessionToken(t, ctx, reg, s) client.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse } diff --git a/session/manager.go b/session/manager.go index d32ad1dbc512..8e0e4cdf6bfc 100644 --- a/session/manager.go +++ b/session/manager.go @@ -7,6 +7,9 @@ import ( "context" "net/http" "net/url" + "time" + + "github.com/ory/kratos/identity" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/text" @@ -141,8 +144,17 @@ type Manager interface { // PurgeFromRequest removes an HTTP session. PurgeFromRequest(context.Context, http.ResponseWriter, *http.Request) error - // DoesSessionSatisfy answers if a session is satisfying the AAL. - DoesSessionSatisfy(r *http.Request, sess *Session, requestedAAL string, opts ...ManagerOptions) error + // DoesSessionSatisfy answers if a session is satisfying the AAL of a user. + // + // The matcher value can be one of: + // + // - `highest_available`: If set requires the user to upgrade their session to the highest available AAL for that user. + // - `aal1`: Requires the user to have authenticated with at least one authentication factor. + // + // This method is implemented in such a way, that if a second factor is found for the user, it is always assumed + // that the user is able to authenticate with it. This means that if a user has a second factor, the user is always + // asked to authenticate with it if `highest_available` is set and the session's AAL is `aal1`. + DoesSessionSatisfy(r *http.Request, sess *Session, matcher string, opts ...ManagerOptions) error // SessionAddAuthenticationMethods adds one or more authentication method to the session. SessionAddAuthenticationMethods(ctx context.Context, sid uuid.UUID, methods ...AuthenticationMethod) error @@ -150,6 +162,13 @@ type Manager interface { // MaybeRedirectAPICodeFlow for API+Code flows redirects the user to the return_to URL and adds the code query parameter. // `handled` is true if the request a redirect was written, false otherwise. MaybeRedirectAPICodeFlow(w http.ResponseWriter, r *http.Request, f flow.Flow, sessionID uuid.UUID, uiNode node.UiNodeGroup) (handled bool, err error) + + // ActivateSession activates a session. + // + // This method is used to activate a session after a user authenticated with a first or second factor. It sets + // all computed values (e.g. authenticator assurance level) and updates the session object but does not store + // the session in the database or on the client device. + ActivateSession(r *http.Request, session *Session, i *identity.Identity, authenticatedAt time.Time) error } type ManagementProvider interface { diff --git a/session/manager_http.go b/session/manager_http.go index 238d15e87fce..b08e21ce0528 100644 --- a/session/manager_http.go +++ b/session/manager_http.go @@ -9,6 +9,8 @@ import ( "net/url" "time" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "github.com/ory/kratos/x/events" @@ -38,6 +40,8 @@ import ( "github.com/ory/kratos/x" ) +var ErrNoAALAvailable = herodot.ErrForbidden.WithReasonf("Unable to detect available authentication methods. Perform account recovery or contact support.") + type ( managerHTTPDependencies interface { config.Provider @@ -45,8 +49,10 @@ type ( identity.PrivilegedPoolProvider identity.ManagementProvider x.CookieProvider + x.LoggingProvider x.CSRFProvider x.TracingProvider + x.TransactionPersistenceProvider PersistenceProvider sessiontokenexchange.PersistenceProvider } @@ -248,7 +254,7 @@ func (s *ManagerHTTP) FetchFromRequest(ctx context.Context, r *http.Request) (_ return nil, errors.WithStack(NewErrNoCredentialsForSession()) } - se, err := s.r.SessionPersister().GetSessionByToken(ctx, token, ExpandEverything, identity.ExpandDefault) + se, err := s.r.SessionPersister().GetSessionByToken(ctx, token, ExpandEverything, identity.ExpandEverything) if err != nil { if errors.Is(err, herodot.ErrNotFound) || errors.Is(err, sqlcon.ErrNoRows) { return nil, errors.WithStack(NewErrNoActiveSessionFound()) @@ -295,69 +301,87 @@ func (s *ManagerHTTP) DoesSessionSatisfy(r *http.Request, sess *Session, request defer otelx.End(span, &err) // If we already have AAL2 there is no need to check further because it is the highest AAL. + sess.SetAuthenticatorAssuranceLevel() if sess.AuthenticatorAssuranceLevel > identity.AuthenticatorAssuranceLevel1 { return nil } managerOpts := &options{} - for _, o := range opts { o(managerOpts) } - sess.SetAuthenticatorAssuranceLevel() + loginURL := urlx.CopyWithQuery(urlx.AppendPaths(s.r.Config().SelfPublicURL(ctx), "/self-service/login/browser"), url.Values{"aal": {"aal2"}}) + + // return to the requestURL if it was set + if managerOpts.requestURL != "" { + loginURL = urlx.CopyWithQuery(loginURL, url.Values{"return_to": {managerOpts.requestURL}}) + } + switch requestedAAL { case string(identity.AuthenticatorAssuranceLevel1): if sess.AuthenticatorAssuranceLevel >= identity.AuthenticatorAssuranceLevel1 { return nil } case config.HighestAvailableAAL: + if sess.AuthenticatorAssuranceLevel >= identity.AuthenticatorAssuranceLevel2 { + // The session has AAL2, nothing to check. + return nil + } + + // The session is AAL1, we asked for `highest_available` AAL, so the only thing we can do + // is actually check what authentication methods the identity has. if sess.Identity == nil { + // This is nil if the session did not expand the identity field. sess.Identity, err = s.r.IdentityPool().GetIdentity(ctx, sess.IdentityID, identity.ExpandNothing) if err != nil { return err } } - i := sess.Identity - available, valid := i.AvailableAAL.ToAAL() - if !valid { - // Available is 0 if the identity was created before the AAL feature was introduced, or if the identity - // was directly created in the persister and not the identity manager. - // - // aal0 indicates that the AAL state of the identity is probably unknown. - // - // In either case, we need to fetch the credentials from the database to determine the AAL. - if len(i.Credentials) == 0 { - // The identity was apparently fetched without credentials. Let's hydrate them. - if err := s.r.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, i, identity.ExpandCredentials); err != nil { - return err - } - } + if aal, ok := sess.Identity.InternalAvailableAAL.ToAAL(); ok && aal == identity.AuthenticatorAssuranceLevel2 { + // Identity gives us AAL2, but the session is still AAL1. We need to upgrade the session. + return NewErrAALNotSatisfied(loginURL.String()) + } + + // Identity AAL is not 2, we refresh: - if err := i.SetAvailableAAL(ctx, s.r.IdentityManager()); err != nil { + // The identity was apparently fetched without credentials. Let's hydrate them. + if len(sess.Identity.Credentials) == 0 { + if err := s.r.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, sess.Identity, identity.ExpandCredentials); err != nil { return err } + } - available, _ = i.AvailableAAL.ToAAL() - - // This is the migration strategy for identities that already exist. - if managerOpts.upsertAAL { - if _, err := s.r.SessionPersister().GetConnection(ctx).Where("id = ? AND nid = ?", i.ID, i.NID).UpdateQuery(i, "available_aal"); err != nil { - return err - } - } + // Great, now we determine the identity's available AAL + if err := sess.Identity.SetAvailableAAL(ctx, s.r.IdentityManager()); err != nil { + return err } - if sess.AuthenticatorAssuranceLevel >= available { - return nil + // We override the result with our newly computed values + available, valid := sess.Identity.InternalAvailableAAL.ToAAL() + if !valid { + // Unlikely to happen because SetAvailableAAL will either return an error, or a valid value - but not no error and an invalid value. + return errors.WithStack(x.PseudoPanic.WithReasonf("Unable to determine available authentication methods for session: %s", sess.ID)) } - loginURL := urlx.CopyWithQuery(urlx.AppendPaths(s.r.Config().SelfPublicURL(ctx), "/self-service/login/browser"), url.Values{"aal": {"aal2"}}) + switch available { + case identity.NoAuthenticatorAssuranceLevel: + // The identity has AAL0, the session has AAL1, we're good. + return nil + case identity.AuthenticatorAssuranceLevel1: + // The identity has AAL1, the session has AAL1, we're good. + return nil + case identity.AuthenticatorAssuranceLevel2: + // The identity has AAL2, the session has AAL1, we need to upgrade the session. - // return to the requestURL if it was set - if managerOpts.requestURL != "" { - loginURL = urlx.CopyWithQuery(loginURL, url.Values{"return_to": {managerOpts.requestURL}}) + // Since we ended up here, it also means that `sess.Identity.InternalAvailableAAL` was `aal1` and is now `aal2`. + // Let's update the database. + if managerOpts.upsertAAL { + if err := s.r.PrivilegedIdentityPool().UpdateIdentityColumns(ctx, sess.Identity, "available_aal"); err != nil { + return err + } + } } return NewErrAALNotSatisfied(loginURL.String()) @@ -414,3 +438,41 @@ func (s *ManagerHTTP) MaybeRedirectAPICodeFlow(w http.ResponseWriter, r *http.Re return true, nil } + +func (s *ManagerHTTP) ActivateSession(r *http.Request, session *Session, i *identity.Identity, authenticatedAt time.Time) (err error) { + ctx, span := s.r.Tracer(r.Context()).Tracer().Start(r.Context(), "sessions.ManagerHTTP.ActivateSession", trace.WithAttributes( + attribute.String("session.id", session.ID.String()), + attribute.String("identity.id", session.ID.String()), + attribute.String("authenticated_at", session.ID.String()), + )) + defer otelx.End(span, &err) + + if i == nil { + return errors.WithStack(x.PseudoPanic.WithReasonf("Identity must not be nil when activating a session.")) + } + + if !i.IsActive() { + return errors.WithStack(ErrIdentityDisabled.WithDetail("identity_id", i.ID)) + } + + if err := s.r.IdentityManager().RefreshAvailableAAL(ctx, i); err != nil { + return err + } + + session.Identity = i + session.IdentityID = i.ID + + session.Active = true + session.IssuedAt = authenticatedAt + session.ExpiresAt = authenticatedAt.Add(s.r.Config().SessionLifespan(ctx)) + session.AuthenticatedAt = authenticatedAt + + session.SetSessionDeviceInformation(r.WithContext(ctx)) + session.SetAuthenticatorAssuranceLevel() + + span.SetAttributes( + attribute.String("identity.available_aal", session.Identity.InternalAvailableAAL.String), + ) + + return nil +} diff --git a/session/manager_http_test.go b/session/manager_http_test.go index 8a1e166da25c..2a6eac16b5b3 100644 --- a/session/manager_http_test.go +++ b/session/manager_http_test.go @@ -13,6 +13,8 @@ import ( "testing" "time" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + "github.com/ory/nosurf" "github.com/ory/x/urlx" @@ -150,6 +152,40 @@ func TestManagerHTTP(t *testing.T) { }) }) + t.Run("suite=SessionActivate", func(t *testing.T) { + req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) + + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + + i := &identity.Identity{ + Traits: []byte("{}"), State: identity.StateActive, + Credentials: map[identity.CredentialsType]identity.Credentials{ + identity.CredentialsTypePassword: {Type: identity.CredentialsTypePassword, Identifiers: []string{x.NewUUID().String()}, Config: []byte(`{"hashed_password":"foo"}`)}, + }, + } + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) + assert.EqualValues(t, i.InternalAvailableAAL.String, "") + + sess := session.NewInactiveSession() + require.NoError(t, reg.SessionManager().ActivateSession(req, sess, i, time.Now().UTC())) + require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), sess)) + + actual, err := reg.SessionPersister().GetSession(context.Background(), sess.ID, session.ExpandEverything) + require.NoError(t, err) + + assert.EqualValues(t, true, actual.Active) + assert.NotZero(t, actual.IssuedAt) + assert.True(t, time.Now().Before(actual.ExpiresAt)) + require.Len(t, actual.Devices, 1) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, i.InternalAvailableAAL.String) + + actualIdentity, err := reg.IdentityPool().GetIdentity(ctx, i.ID, identity.ExpandNothing) + require.NoError(t, err) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, actualIdentity.InternalAvailableAAL.String) + + }) + t.Run("suite=SessionAddAuthenticationMethod", func(t *testing.T) { req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) @@ -159,7 +195,7 @@ func TestManagerHTTP(t *testing.T) { i := &identity.Identity{Traits: []byte("{}"), State: identity.StateActive} require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i)) sess := session.NewInactiveSession() - require.NoError(t, sess.Activate(req, i, conf, time.Now())) + require.NoError(t, reg.SessionManager().ActivateSession(req, sess, i, time.Now().UTC())) require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), sess)) require.NoError(t, reg.SessionManager().SessionAddAuthenticationMethods(context.Background(), sess.ID, session.AuthenticationMethod{ @@ -219,7 +255,7 @@ func TestManagerHTTP(t *testing.T) { i := identity.Identity{Traits: []byte("{}")} require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i)) - s, _ = session.NewActiveSession(req, &i, conf, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + s, _ = testhelpers.NewActiveSession(req, reg, &i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) c := testhelpers.NewClientWithCookies(t) testhelpers.MockHydrateCookieClient(t, c, pts.URL+"/session/set") @@ -240,7 +276,7 @@ func TestManagerHTTP(t *testing.T) { i := identity.Identity{Traits: []byte("{}")} require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i)) - s, _ = session.NewActiveSession(req, &i, conf, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + s, _ = testhelpers.NewActiveSession(req, reg, &i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) c := testhelpers.NewClientWithCookies(t) testhelpers.MockHydrateCookieClient(t, c, pts.URL+"/session/set") @@ -270,7 +306,7 @@ func TestManagerHTTP(t *testing.T) { i := identity.Identity{Traits: []byte("{}")} require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i)) - s, _ = session.NewActiveSession(req, &i, conf, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + s, _ = testhelpers.NewActiveSession(req, reg, &i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) c := testhelpers.NewClientWithCookies(t) res, err := c.Get(pts.URL + "/session/set/invalid") @@ -284,7 +320,7 @@ func TestManagerHTTP(t *testing.T) { i := identity.Identity{Traits: []byte("{}"), State: identity.StateActive} require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i)) - s, err := session.NewActiveSession(req, &i, conf, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + s, err := testhelpers.NewActiveSession(req, reg, &i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) require.NoError(t, err) require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), s)) require.NotEmpty(t, s.Token) @@ -305,7 +341,7 @@ func TestManagerHTTP(t *testing.T) { i := identity.Identity{Traits: []byte("{}"), State: identity.StateActive} require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i)) - s, err := session.NewActiveSession(req, &i, conf, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + s, err := testhelpers.NewActiveSession(req, reg, &i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) require.NoError(t, err) require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), s)) @@ -329,7 +365,7 @@ func TestManagerHTTP(t *testing.T) { i := identity.Identity{Traits: []byte("{}")} require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i)) - s, _ = session.NewActiveSession(req, &i, conf, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + s, _ = testhelpers.NewActiveSession(req, reg, &i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) c := testhelpers.NewClientWithCookies(t) testhelpers.MockHydrateCookieClient(t, c, pts.URL+"/session/set") @@ -345,9 +381,9 @@ func TestManagerHTTP(t *testing.T) { req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) i := identity.Identity{Traits: []byte("{}")} require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i)) - s, _ = session.NewActiveSession(req, &i, conf, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + s, _ = testhelpers.NewActiveSession(req, reg, &i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) - s, _ = session.NewActiveSession(req, &i, conf, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + s, _ = testhelpers.NewActiveSession(req, reg, &i, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) c := testhelpers.NewClientWithCookies(t) testhelpers.MockHydrateCookieClient(t, c, pts.URL+"/session/set") @@ -371,7 +407,7 @@ func TestManagerHTTP(t *testing.T) { for _, m := range complete { s.CompletedLoginFor(m, "") } - require.NoError(t, s.Activate(req, i, conf, time.Now().UTC())) + require.NoError(t, reg.SessionManager().ActivateSession(req, s, i, time.Now().UTC())) err := reg.SessionManager().DoesSessionSatisfy((&http.Request{}).WithContext(context.Background()), s, requested) if expectedError != nil { require.ErrorAs(t, err, &expectedError) @@ -418,22 +454,22 @@ func TestManagerHTTP(t *testing.T) { s := session.NewInactiveSession() s.CompletedLoginFor(identity.CredentialsTypePassword, "") - require.NoError(t, s.Activate(req, idAAL1, conf, time.Now().UTC())) + require.NoError(t, reg.SessionManager().ActivateSession(req, s, idAAL1, time.Now().UTC())) require.Error(t, reg.SessionManager().DoesSessionSatisfy((&http.Request{}).WithContext(context.Background()), s, config.HighestAvailableAAL, session.UpsertAAL)) result, err := reg.IdentityPool().GetIdentity(context.Background(), idAAL1.ID, identity.ExpandNothing) require.NoError(t, err) - assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, result.AvailableAAL.String) + assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, result.InternalAvailableAAL.String) }) t.Run("identity available AAL is hydrated without DB", func(t *testing.T) { // We do not create the identity in the database, proving that we do not need // to do any DB roundtrips in this case. idAAL2 := createAAL2Identity(t, reg) - idAAL2.AvailableAAL = identity.NewNullableAuthenticatorAssuranceLevel(identity.AuthenticatorAssuranceLevel2) + idAAL2.InternalAvailableAAL = identity.NewNullableAuthenticatorAssuranceLevel(identity.AuthenticatorAssuranceLevel2) idAAL1 := createAAL1Identity(t, reg) - idAAL1.AvailableAAL = identity.NewNullableAuthenticatorAssuranceLevel(identity.AuthenticatorAssuranceLevel1) + idAAL1.InternalAvailableAAL = identity.NewNullableAuthenticatorAssuranceLevel(identity.AuthenticatorAssuranceLevel1) test(t, idAAL1, idAAL2) }) @@ -443,19 +479,76 @@ func TestManagerHTTP(t *testing.T) { } func TestDoesSessionSatisfy(t *testing.T) { + ctx := context.Background() conf, reg := internal.NewFastRegistryWithMocks(t) testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") + ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/identity.schema.json") + passwordEmpty := identity.Credentials{Type: identity.CredentialsTypePassword, Config: []byte(`{}`), Identifiers: []string{testhelpers.RandomEmail()}} password := identity.Credentials{ Type: identity.CredentialsTypePassword, Identifiers: []string{testhelpers.RandomEmail()}, Config: []byte(`{"hashed_password": "$argon2id$v=19$m=32,t=2,p=4$cm94YnRVOW5jZzFzcVE4bQ$MNzk5BtR2vUhrp6qQEjRNw"}`), } + passwordMigration := identity.Credentials{ + Type: identity.CredentialsTypePassword, + Identifiers: []string{testhelpers.RandomEmail()}, + Config: []byte(`{"use_password_migration_hook":true}`), + } + + code := identity.Credentials{ + Type: identity.CredentialsTypeCodeAuth, + Identifiers: []string{testhelpers.RandomEmail()}, + Config: []byte(`{"address_type":"email","used_at":{"Time":"0001-01-01T00:00:00Z","Valid":false}}`), + } + //codeEmpty := identity.Credentials{ + // Type: identity.CredentialsTypeCodeAuth, + // Identifiers: []string{testhelpers.RandomEmail()}, + // Config: []byte(`{}`), + //} + oidc := identity.Credentials{ Type: identity.CredentialsTypeOIDC, Config: []byte(`{"providers":[{"subject":"0.fywegkf7hd@ory.sh","provider":"hydra","initial_id_token":"65794a68624763694f694a53557a49314e694973496d74705a434936496e4231596d7870597a706f6557527959533576634756756157517561575174644739725a5734694c434a30655841694f694a4b5631516966512e65794a686446396f59584e6f496a6f6956484650616b6f324e6c397a613046436555643662315679576b466655534973496d46315a43493657794a72636d463062334d74593278705a573530496c3073496d46316447686664476c745a5349364d5459304e6a55314e6a59784e4377695a586877496a6f784e6a51324e5459774d6a45314c434a70595851694f6a45324e4459314e5459324d545573496d6c7a63794936496d6830644841364c79397362324e6862476876633351364e4451304e4338694c434a7164476b694f694a6a596a4d784d6a51794e6930314e7a4d774c5451314d546374596a51335a53316b4d446379596a51334d6a6b344d4759694c434a79595851694f6a45324e4459314e5459324d544d73496e4e705a434936496a677a4e5755344e47526a4c5463344d544d744e4749324f4330354d544a6d4c5446684d7a646d4e444d354d4463304e534973496e4e3159694936496a41755a6e6c335a5764725a6a646f5a454276636e6b75633267694c434a335a574a7a6158526c496a6f696148523063484d364c7939336433637562334a354c6e4e6f4c794a392e506850623770456358544c3456647730427959686f30794a7232714b794b4f7373646c4b6c74716b4953693762414e58776a7635686538506e6d7a586e713538556f5739657754584a485a33425651614d4e79612d755f5933584a4a61665673543347476c52776f376f5261707a6a564836502d72447657385649524d5361356f783242397164416d796659505734376e56782d4e68787247564c56464b526b5866324e4448534e6d435968524963455539724331366235385331344c314367776972624d507662797870644c63764f4a4546554238324c794574525a786f644748354c69394d6b5f4d6137363969583254776758434179306734475a625957337137317466574c37736d5342394669785076434b6a3738433753546b762d764f737a4e6533523864676133775471466e6253797a6a614f4b47626e424a4a77423869306e416c48496d425337587146645f666d556d4e62377a372d63716e593374395069306248466b46596e6746545279664d4c6f466f576956784842704b4d6c6b304d4e7a5155414e5368546e346769544d5547454a4f6372346f6f445f6770344768734c44542d54465f6f73486c304832544237777a6d546d735f3150506547424e716a316b61576a467038567247726e4a6b354f594c643152473152464c794535544c4d47315f62744762447137334450784c334b3657387348507242504b654133344377373371584e5247724e73574e69496e775f4e596a65554d484b6351436c4e51445a49725339794962456a485a78476a34546e4367664f5974694e76527a4c6c36616a73614265464b7a45592d6348416e6e42694c75744439373168697241684f5463544a42783672716f67717764755356726551456f565a5735616e4a7a7575775234685453354d44314d64457045437471526d416c71555459644e5a365778514d","initial_access_token":"52344752743736552d634a2d4a2d424372447159634967464652446c6455455a6a526e534d62336e3242732e47324f444d64303544774b4e67395649476e306e496b3877324e72444f48384a78635042635a4a58336d63","initial_refresh_token":"327872337a4d382d654273674b6d61644a624e5a497572473374545154615070313264514a314476544d632e77326d34747a6e7950584c38324b794563716468685068635156314f77386a535a345355496f3544744a51"}]}`), Identifiers: []string{"hydra:0.fywegkf7hd@ory.sh"}, } + //oidcEmpty := identity.Credentials{ + // Type: identity.CredentialsTypeOIDC, + // Config: []byte(`{}`), + // Identifiers: []string{"hydra:0.fywegkf7hd@ory.sh"}, + //} + + lookupSecrets := identity.Credentials{ + Type: identity.CredentialsTypeLookup, + Config: []byte(`{"recovery_codes": [{"code": "abcde", "used_at": null}]}`), + } + //lookupSecretsEmpty := identity.Credentials{ + // Type: identity.CredentialsTypeLookup, + // Config: []byte(`{}`), + //} + + totp := identity.Credentials{ + Type: identity.CredentialsTypeTOTP, + Config: []byte(`{"totp_url": "otpauth://totp/..."}`), + } + //totpEmpty := identity.Credentials{ + // Type: identity.CredentialsTypeTOTP, + // Config: []byte(`{}`), + //} + + // passkey + passkey := identity.Credentials{ // passkey + Type: identity.CredentialsTypePasskey, + Config: []byte(`{"credentials":[{}]}`), + Identifiers: []string{testhelpers.RandomEmail()}, + } + //passkeyEmpty := identity.Credentials{ // passkey + // Type: identity.CredentialsTypePasskey, + // Config: []byte(`{"credentials":null}`), + // Identifiers: []string{testhelpers.RandomEmail()}, + //} + + // webAuthn mfaWebAuth := identity.Credentials{ Type: identity.CredentialsTypeWebAuthn, Config: []byte(`{"credentials":[{"is_passwordless":false}]}`), @@ -467,106 +560,265 @@ func TestDoesSessionSatisfy(t *testing.T) { Identifiers: []string{testhelpers.RandomEmail()}, } webAuthEmpty := identity.Credentials{Type: identity.CredentialsTypeWebAuthn, Config: []byte(`{}`), Identifiers: []string{testhelpers.RandomEmail()}} - passwordEmpty := identity.Credentials{Type: identity.CredentialsTypePassword, Config: []byte(`{}`), Identifiers: []string{testhelpers.RandomEmail()}} - amrPassword := session.AuthenticationMethod{Method: identity.CredentialsTypePassword, AAL: identity.AuthenticatorAssuranceLevel1} + amrs := map[identity.CredentialsType]session.AuthenticationMethod{} + for _, strat := range reg.AllLoginStrategies() { + amrs[strat.ID()] = strat.CompletedAuthenticationMethod(ctx) + } for k, tc := range []struct { - d string - err error - requested identity.AuthenticatorAssuranceLevel + desc string + withContext func(*testing.T, context.Context) context.Context + errAs error + errIs error + matcher identity.AuthenticatorAssuranceLevel creds []identity.Credentials - amr session.AuthenticationMethods + withAMR session.AuthenticationMethods sessionManagerOptions []session.ManagerOptions expectedFunc func(t *testing.T, err error, tcError error) }{ { - d: "has=aal1, requested=highest, available=aal1, credential=password", - requested: config.HighestAvailableAAL, - creds: []identity.Credentials{password}, - amr: session.AuthenticationMethods{amrPassword}, + desc: "with highest_available a password user is aal1", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePassword]}, + // No error }, { - d: "has=aal1, requested=highest, available=aal1, credential=password, legacy=true", - requested: config.HighestAvailableAAL, - creds: []identity.Credentials{password}, - amr: session.AuthenticationMethods{{Method: identity.CredentialsTypePassword}}, + desc: "with highest_available a password migration user is aal1 if password migration is enabled", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{passwordMigration}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePassword]}, + withContext: func(t *testing.T, ctx context.Context) context.Context { + return confighelpers.WithConfigValues(ctx, map[string]any{ + "selfservice.methods.password_migration.enabled": true, + }) + }, + // No error }, { - d: "has=aal1, requested=highest, available=aal1, credential=password+webauth_empty", - requested: config.HighestAvailableAAL, - creds: []identity.Credentials{password, webAuthEmpty}, - amr: session.AuthenticationMethods{amrPassword}, + // This is not an error because DoesSessionSatisfy always assumes at least aal1 + desc: "with highest_available a password migration user is aal1 if password migration is disabled", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{passwordMigration}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePassword]}, + withContext: func(t *testing.T, ctx context.Context) context.Context { + return confighelpers.WithConfigValues(ctx, map[string]any{ + "selfservice.methods.password_migration.enabled": false, + }) + }, + // No error }, { - d: "has=aal1, requested=highest, available=aal1, credential=password+webauth_empty, legacy=true", - requested: config.HighestAvailableAAL, - creds: []identity.Credentials{password, webAuthEmpty}, - amr: session.AuthenticationMethods{{Method: identity.CredentialsTypePassword}}, + desc: "with highest_available a otp code user is aal1", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{code}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypeCodeAuth]}, + // No error }, { - d: "has=aal1, requested=highest, available=aal1, credential=password+webauth_passwordless", - requested: config.HighestAvailableAAL, - creds: []identity.Credentials{password, passwordlessWebAuth}, - amr: session.AuthenticationMethods{amrPassword}, + desc: "with highest_available a oidc user is aal1", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{oidc}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypeOIDC]}, + // No error }, { - d: "has=aal1, requested=highest, available=aal1, credential=password+webauth_passwordless, legacy=true", - requested: config.HighestAvailableAAL, - creds: []identity.Credentials{password, passwordlessWebAuth}, - amr: session.AuthenticationMethods{{Method: identity.CredentialsTypePassword}}, + desc: "with highest_available a passkey user is aal1", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{passkey}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePasskey]}, + // No error }, { - d: "has=aal1, requested=highest, available=aal2, credential=password+webauth_mfa", - requested: config.HighestAvailableAAL, - creds: []identity.Credentials{password, mfaWebAuth}, - amr: session.AuthenticationMethods{amrPassword}, - err: new(session.ErrAALNotSatisfied), + desc: "with highest_available a recovery token user is aal1 even if they have no credentials", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypeRecoveryLink]}, + // No error }, { - d: "has=aal1, requested=highest, available=aal2, credential=password+webauth_mfa, legacy=true", - requested: config.HighestAvailableAAL, - creds: []identity.Credentials{password, mfaWebAuth}, - amr: session.AuthenticationMethods{{Method: identity.CredentialsTypePassword}}, - err: new(session.ErrAALNotSatisfied), + desc: "with highest_available a recovery code user is aal1 even if they have no credentials", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypeRecoveryCode]}, + // No error }, + // Test a recovery method with an identity that has only 2fa methods enabled. { - d: "has=aal1, requested=highest, available=aal2, credential=password+webauth_mfa", - requested: config.HighestAvailableAAL, - creds: []identity.Credentials{password, mfaWebAuth}, - amr: session.AuthenticationMethods{amrPassword, {Method: identity.CredentialsTypeWebAuthn, AAL: identity.AuthenticatorAssuranceLevel1}}, - err: new(session.ErrAALNotSatisfied), + desc: "with highest_available a recovery link user requires aal2 if they have 2fa totp configured", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{totp}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypeRecoveryLink]}, + errIs: new(session.ErrAALNotSatisfied), }, { - d: "has=aal1, requested=highest, available=aal2, credential=password+webauth_passwordless", - requested: config.HighestAvailableAAL, - creds: []identity.Credentials{password, passwordlessWebAuth}, - amr: session.AuthenticationMethods{amrPassword, {Method: identity.CredentialsTypeWebAuthn, AAL: identity.AuthenticatorAssuranceLevel1}}, + desc: "with highest_available a recovery code user requires aal2 if they have 2fa lookup configured", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{lookupSecrets}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypeRecoveryCode]}, + errIs: new(session.ErrAALNotSatisfied), }, { - d: "has=aal2, requested=highest, available=aal2, credential=password+webauth_mfa", - requested: config.HighestAvailableAAL, - creds: []identity.Credentials{password, mfaWebAuth}, - amr: session.AuthenticationMethods{amrPassword, {Method: identity.CredentialsTypeWebAuthn, AAL: identity.AuthenticatorAssuranceLevel2}}, + desc: "with highest_available a recovery code user requires aal2 if they have 2fa lookup configured", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{mfaWebAuth}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypeRecoveryCode]}, + errIs: new(session.ErrAALNotSatisfied), }, { - d: "has=aal2, requested=highest, available=aal2, credential=password+webauth_mfa, legacy=true", - requested: config.HighestAvailableAAL, - creds: []identity.Credentials{password, mfaWebAuth}, - amr: session.AuthenticationMethods{amrPassword, {Method: identity.CredentialsTypeWebAuthn}}, + desc: "with highest_available a recovery code user requires aal2 if they have many 2fa methods configured", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{lookupSecrets, mfaWebAuth, totp}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypeRecoveryCode]}, + errIs: new(session.ErrAALNotSatisfied), }, { - d: "has=aal1, requested=highest, available=aal1, credential=oidc_and_empties", - requested: config.HighestAvailableAAL, - creds: []identity.Credentials{oidc, webAuthEmpty, passwordEmpty}, - amr: session.AuthenticationMethods{{Method: identity.CredentialsTypeOIDC, AAL: identity.AuthenticatorAssuranceLevel1}}, + desc: "with highest_available a recovery link user requires aal2 if they have 2fa code configured", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypeRecoveryLink]}, + withContext: func(t *testing.T, ctx context.Context) context.Context { + return confighelpers.WithConfigValues(ctx, map[string]any{ + "selfservice.methods.code.passwordless_enabled": false, + "selfservice.methods.code.mfa_enabled": true, + }) + }, + errIs: new(session.ErrAALNotSatisfied), }, + + // Legacy tests { - d: "has=aal1, requested=highest, available=aal1, credentials=password+webauthn_mfa, recovery with session manager options", - requested: config.HighestAvailableAAL, + desc: "has=aal1, requested=highest, available=aal0, credential=code", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{totp}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypeRecoveryCode]}, + errIs: session.ErrNoAALAvailable, + }, + + { + desc: "has=aal1, requested=highest, available=aal1, credential=password", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePassword]}, + }, + { + desc: "has=aal1, requested=highest, available=aal1, credential=password, legacy=true", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password}, + withAMR: session.AuthenticationMethods{{Method: identity.CredentialsTypePassword}}, + }, + { + desc: "has=aal1, requested=highest, available=aal1, credential=password+webauth_empty", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password, webAuthEmpty}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePassword]}, + }, + { + desc: "has=aal1, requested=highest, available=aal1, credential=password+webauth_empty, legacy=true", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password, webAuthEmpty}, + withAMR: session.AuthenticationMethods{{Method: identity.CredentialsTypePassword}}, + }, + { + desc: "has=aal1, requested=highest, available=aal1, credential=password+webauth_passwordless", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password, passwordlessWebAuth}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePassword]}, + }, + { + desc: "has=aal1, requested=highest, available=aal1, credential=password+webauth_passwordless, legacy=true", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password, passwordlessWebAuth}, + withAMR: session.AuthenticationMethods{{Method: identity.CredentialsTypePassword}}, + }, + { + desc: "has=aal1, requested=highest, available=aal2, credential=password+webauth_mfa", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password, mfaWebAuth}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePassword]}, + errAs: new(session.ErrAALNotSatisfied), + }, + { + desc: "has=aal1, requested=highest, available=aal2, credential=password+totp", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password, totp}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePassword]}, + errAs: new(session.ErrAALNotSatisfied), + }, + { + desc: "has=aal1, requested=highest, available=aal2, credential=password+code-mfa", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password, code}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePassword]}, + errAs: new(session.ErrAALNotSatisfied), + withContext: func(t *testing.T, ctx context.Context) context.Context { + return confighelpers.WithConfigValues(ctx, map[string]any{ + "selfservice.methods.code.passwordless_enabled": false, + "selfservice.methods.code.mfa_enabled": true, + }) + }, + }, + { + desc: "has=aal1, requested=highest, available=aal2, credential=password+lookup_secrets", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password, lookupSecrets}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePassword]}, + errAs: new(session.ErrAALNotSatisfied), + }, + { + desc: "has=aal1, requested=highest, available=aal2, credential=password+webauth_mfa, legacy=true", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password, mfaWebAuth}, + withAMR: session.AuthenticationMethods{{Method: identity.CredentialsTypePassword}}, + errAs: new(session.ErrAALNotSatisfied), + }, + { + desc: "has=aal1, requested=highest, available=aal2, credential=password+webauth_mfa", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password, mfaWebAuth}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePassword], {Method: identity.CredentialsTypeWebAuthn, AAL: identity.AuthenticatorAssuranceLevel1}}, + errAs: new(session.ErrAALNotSatisfied), + }, + { + desc: "has=aal1, requested=highest, available=aal2, credential=password+webauth_passwordless", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password, passwordlessWebAuth}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePassword], {Method: identity.CredentialsTypeWebAuthn, AAL: identity.AuthenticatorAssuranceLevel1}}, + }, + { + desc: "has=aal2, requested=highest, available=aal2, credential=password+webauth_mfa", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password, mfaWebAuth}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePassword], {Method: identity.CredentialsTypeWebAuthn, AAL: identity.AuthenticatorAssuranceLevel2}}, + }, + { + desc: "has=aal2, requested=highest, available=aal2, credential=password+webauth_mfa, legacy=true", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password, mfaWebAuth}, + withAMR: session.AuthenticationMethods{amrs[identity.CredentialsTypePassword], {Method: identity.CredentialsTypeWebAuthn}}, + }, + + // oidc + { + desc: "has=aal1, requested=highest, available=aal1, credential=oidc_and_empties", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{oidc, webAuthEmpty, passwordEmpty}, + withAMR: session.AuthenticationMethods{{Method: identity.CredentialsTypeOIDC, AAL: identity.AuthenticatorAssuranceLevel1}}, + }, + { + desc: "has=aal1, requested=highest, available=aal1, credential=code and totp", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{code, totp}, + withAMR: session.AuthenticationMethods{{Method: identity.CredentialsTypeCodeAuth, AAL: identity.AuthenticatorAssuranceLevel1}}, + errAs: session.NewErrAALNotSatisfied(urlx.CopyWithQuery(urlx.AppendPaths(conf.SelfPublicURL(ctx), "/self-service/login/browser"), url.Values{"aal": {"aal2"}, "return_to": {"https://myapp.com/settings?id=123"}}).String()), + }, + { + desc: "has=aal1, requested=highest, available=aal1, credentials=password+webauthn_mfa, recovery with session manager options", + matcher: config.HighestAvailableAAL, creds: []identity.Credentials{password, mfaWebAuth}, - amr: session.AuthenticationMethods{{Method: identity.CredentialsTypeRecoveryCode}}, - err: session.NewErrAALNotSatisfied(urlx.CopyWithQuery(urlx.AppendPaths(conf.SelfPublicURL(context.Background()), "/self-service/login/browser"), url.Values{"aal": {"aal2"}, "return_to": {"https://myapp.com/settings?id=123"}}).String()), + withAMR: session.AuthenticationMethods{{Method: identity.CredentialsTypeRecoveryCode}}, + errAs: session.NewErrAALNotSatisfied(urlx.CopyWithQuery(urlx.AppendPaths(conf.SelfPublicURL(ctx), "/self-service/login/browser"), url.Values{"aal": {"aal2"}, "return_to": {"https://myapp.com/settings?id=123"}}).String()), sessionManagerOptions: []session.ManagerOptions{session.WithRequestURL("https://myapp.com/settings?id=123")}, expectedFunc: func(t *testing.T, err error, tcError error) { require.Contains(t, err.(*session.ErrAALNotSatisfied).RedirectTo, "myapp.com") @@ -574,64 +826,74 @@ func TestDoesSessionSatisfy(t *testing.T) { }, }, { - d: "has=aal1, requested=highest, available=aal1, credentials=password+webauthn_mfa, recovery without session manager options", - requested: config.HighestAvailableAAL, - creds: []identity.Credentials{password, mfaWebAuth}, - amr: session.AuthenticationMethods{{Method: identity.CredentialsTypeRecoveryCode}}, - err: session.NewErrAALNotSatisfied(urlx.CopyWithQuery(urlx.AppendPaths(conf.SelfPublicURL(context.Background()), "/self-service/login/browser"), url.Values{"aal": {"aal2"}}).String()), + desc: "has=aal1, requested=highest, available=aal1, credentials=password+webauthn_mfa, recovery without session manager options", + matcher: config.HighestAvailableAAL, + creds: []identity.Credentials{password, mfaWebAuth}, + withAMR: session.AuthenticationMethods{{Method: identity.CredentialsTypeRecoveryCode}}, + errAs: session.NewErrAALNotSatisfied(urlx.CopyWithQuery(urlx.AppendPaths(conf.SelfPublicURL(ctx), "/self-service/login/browser"), url.Values{"aal": {"aal2"}}).String()), expectedFunc: func(t *testing.T, err error, tcError error) { require.Equal(t, tcError.(*session.ErrAALNotSatisfied).RedirectTo, err.(*session.ErrAALNotSatisfied).RedirectTo) }, }, } { - t.Run(fmt.Sprintf("run=%d/desc=%s", k, tc.d), func(t *testing.T) { - id := identity.NewIdentity("") + t.Run(fmt.Sprintf("run=%d/desc=%s", k, tc.desc), func(t *testing.T) { + ctx := ctx + if tc.withContext != nil { + ctx = tc.withContext(t, ctx) + } + + id := identity.NewIdentity("default") for _, c := range tc.creds { id.SetCredentials(c.Type, c) } - require.NoError(t, reg.IdentityManager().Create(context.Background(), id, identity.ManagerAllowWriteProtectedTraits)) + require.NoError(t, reg.IdentityManager().Create(ctx, id, identity.ManagerAllowWriteProtectedTraits)) t.Cleanup(func() { - require.NoError(t, reg.PrivilegedIdentityPool().DeleteIdentity(context.Background(), id.ID)) + require.NoError(t, reg.PrivilegedIdentityPool().DeleteIdentity(ctx, id.ID)) }) req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) s := session.NewInactiveSession() - for _, m := range tc.amr { + for _, m := range tc.withAMR { s.CompletedLoginFor(m.Method, m.AAL) } - require.NoError(t, s.Activate(req, id, conf, time.Now().UTC())) + require.NoError(t, reg.SessionManager().ActivateSession(req, s, id, time.Now().UTC())) - err := reg.SessionManager().DoesSessionSatisfy((&http.Request{}).WithContext(context.Background()), s, string(tc.requested), tc.sessionManagerOptions...) - if tc.err != nil { + err := reg.SessionManager().DoesSessionSatisfy((&http.Request{}).WithContext(ctx), s, string(tc.matcher), tc.sessionManagerOptions...) + if tc.errAs != nil || tc.errIs != nil { + if tc.expectedFunc != nil { + tc.expectedFunc(t, err, tc.errAs) + } + require.ErrorAs(t, err, &tc.errAs) + } else if tc.errIs != nil { if tc.expectedFunc != nil { - tc.expectedFunc(t, err, tc.err) + tc.expectedFunc(t, err, tc.errIs) } - require.ErrorAs(t, err, &tc.err) + require.ErrorIs(t, err, tc.errIs) } else { require.NoError(t, err) } - // This should still work even if the session does not have identity data attached yet... + // This should still work even if the session does not have identity data attached yet ... s.Identity = nil - err = reg.SessionManager().DoesSessionSatisfy((&http.Request{}).WithContext(context.Background()), s, string(tc.requested), tc.sessionManagerOptions...) - if tc.err != nil { + err = reg.SessionManager().DoesSessionSatisfy((&http.Request{}).WithContext(ctx), s, string(tc.matcher), tc.sessionManagerOptions...) + if tc.errAs != nil { if tc.expectedFunc != nil { - tc.expectedFunc(t, err, tc.err) + tc.expectedFunc(t, err, tc.errAs) } - require.ErrorAs(t, err, &tc.err) + require.ErrorAs(t, err, &tc.errAs) } else { require.NoError(t, err) } - // ..or no credentials attached. + // ... or no credentials attached. s.Identity = id s.Identity.Credentials = nil - err = reg.SessionManager().DoesSessionSatisfy((&http.Request{}).WithContext(context.Background()), s, string(tc.requested), tc.sessionManagerOptions...) - if tc.err != nil { + err = reg.SessionManager().DoesSessionSatisfy((&http.Request{}).WithContext(ctx), s, string(tc.matcher), tc.sessionManagerOptions...) + if tc.errAs != nil { if tc.expectedFunc != nil { - tc.expectedFunc(t, err, tc.err) + tc.expectedFunc(t, err, tc.errAs) } - require.ErrorAs(t, err, &tc.err) + require.ErrorAs(t, err, &tc.errAs) } else { require.NoError(t, err) } diff --git a/session/persistence.go b/session/persistence.go index 34bf4d2654d4..ab5ec0a47735 100644 --- a/session/persistence.go +++ b/session/persistence.go @@ -35,6 +35,9 @@ type Persister interface { // UpsertSession inserts or updates a session into / in the store. UpsertSession(ctx context.Context, s *Session) error + // ExtendSession updates the expiry of a session. + ExtendSession(ctx context.Context, sessionID uuid.UUID) error + // DeleteSession removes a session from the store. DeleteSession(ctx context.Context, id uuid.UUID) error diff --git a/session/session.go b/session/session.go index 95ad5e06b8ae..910c0d3d335a 100644 --- a/session/session.go +++ b/session/session.go @@ -16,7 +16,7 @@ import ( "github.com/ory/x/httpx" "github.com/ory/x/pagination/keysetpagination" - "github.com/ory/x/stringsx" + "github.com/ory/x/pointerx" "github.com/pkg/errors" @@ -227,6 +227,9 @@ func (s *Session) SetAuthenticatorAssuranceLevel() { isAAL1 = true case identity.AuthenticatorAssuranceLevel2: isAAL2 = true + // The following section is a graceful migration from Ory Kratos v0.9. + // + // TODO remove this section, it is already over 2 years old. case "": // Sessions before Ory Kratos 0.9 did not have the AAL // be part of the AMR. @@ -255,20 +258,11 @@ func (s *Session) SetAuthenticatorAssuranceLevel() { } else if isAAL1 { s.AuthenticatorAssuranceLevel = identity.AuthenticatorAssuranceLevel1 } else if len(s.AMR) > 0 { - // A fallback. If an AMR is set but we did not satisfy the above, gracefully fall back to level 1. + // A fallback. If an AMR is set, but we did not satisfy the above, gracefully fall back to level 1. s.AuthenticatorAssuranceLevel = identity.AuthenticatorAssuranceLevel1 } } -func NewActiveSession(r *http.Request, i *identity.Identity, c lifespanProvider, authenticatedAt time.Time, completedLoginFor identity.CredentialsType, completedLoginAAL identity.AuthenticatorAssuranceLevel) (*Session, error) { - s := NewInactiveSession() - s.CompletedLoginFor(completedLoginFor, completedLoginAAL) - if err := s.Activate(r, i, c, authenticatedAt); err != nil { - return nil, err - } - return s, nil -} - func NewInactiveSession() *Session { return &Session{ ID: uuid.Nil, @@ -279,32 +273,15 @@ func NewInactiveSession() *Session { } } -func (s *Session) Activate(r *http.Request, i *identity.Identity, c lifespanProvider, authenticatedAt time.Time) error { - if i != nil && !i.IsActive() { - return ErrIdentityDisabled.WithDetail("identity_id", i.ID) - } - - s.Active = true - s.ExpiresAt = authenticatedAt.Add(c.SessionLifespan(r.Context())) - s.AuthenticatedAt = authenticatedAt - s.IssuedAt = authenticatedAt - s.Identity = i - s.IdentityID = i.ID - - s.SetSessionDeviceInformation(r) - s.SetAuthenticatorAssuranceLevel() - return nil -} - func (s *Session) SetSessionDeviceInformation(r *http.Request) { device := Device{ SessionID: s.ID, - IPAddress: stringsx.GetPointer(httpx.ClientIP(r)), + IPAddress: pointerx.Ptr(httpx.ClientIP(r)), } agent := r.Header["User-Agent"] if len(agent) > 0 { - device.UserAgent = stringsx.GetPointer(strings.Join(agent, " ")) + device.UserAgent = pointerx.Ptr(strings.Join(agent, " ")) } var clientGeoLocation []string @@ -314,7 +291,7 @@ func (s *Session) SetSessionDeviceInformation(r *http.Request) { if r.Header.Get("Cf-Ipcountry") != "" { clientGeoLocation = append(clientGeoLocation, r.Header.Get("Cf-Ipcountry")) } - device.Location = stringsx.GetPointer(strings.Join(clientGeoLocation, ", ")) + device.Location = pointerx.Ptr(strings.Join(clientGeoLocation, ", ")) s.Devices = append(s.Devices, device) } diff --git a/session/session_test.go b/session/session_test.go index 4e1efe3b647a..75fc61ea300a 100644 --- a/session/session_test.go +++ b/session/session_test.go @@ -9,6 +9,8 @@ import ( "testing" "time" + "github.com/ory/kratos/x" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/assert" @@ -22,7 +24,8 @@ import ( func TestSession(t *testing.T) { ctx := context.Background() - conf, _ := internal.NewFastRegistryWithMocks(t) + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") authAt := time.Now() t.Run("case=active session", func(t *testing.T) { @@ -30,14 +33,17 @@ func TestSession(t *testing.T) { i := new(identity.Identity) i.State = identity.StateActive - s, _ := session.NewActiveSession(req, i, conf, authAt, identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + i.NID = x.NewUUID() + s, err := testhelpers.NewActiveSession(req, reg, i, authAt, identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) assert.True(t, s.IsActive()) require.NotEmpty(t, s.Token) require.NotEmpty(t, s.LogoutToken) assert.EqualValues(t, identity.CredentialsTypePassword, s.AMR[0].Method) i = new(identity.Identity) - s, err := session.NewActiveSession(req, i, conf, authAt, identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + i.NID = x.NewUUID() + s, err = testhelpers.NewActiveSession(req, reg, i, authAt, identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) assert.Nil(t, s) assert.ErrorIs(t, err, session.ErrIdentityDisabled) }) @@ -62,13 +68,13 @@ func TestSession(t *testing.T) { req := testhelpers.NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil) s := session.NewInactiveSession() - require.NoError(t, s.Activate(req, &identity.Identity{State: identity.StateActive}, conf, authAt)) + require.NoError(t, reg.SessionManager().ActivateSession(req, s, &identity.Identity{NID: x.NewUUID(), State: identity.StateActive}, authAt)) assert.True(t, s.Active) assert.Equal(t, identity.NoAuthenticatorAssuranceLevel, s.AuthenticatorAssuranceLevel) assert.Equal(t, authAt, s.AuthenticatedAt) s = session.NewInactiveSession() - require.ErrorIs(t, s.Activate(req, &identity.Identity{State: identity.StateInactive}, conf, authAt), session.ErrIdentityDisabled) + require.ErrorIs(t, reg.SessionManager().ActivateSession(req, s, &identity.Identity{NID: x.NewUUID(), State: identity.StateInactive}, authAt), session.ErrIdentityDisabled) assert.False(t, s.Active) assert.Equal(t, identity.NoAuthenticatorAssuranceLevel, s.AuthenticatorAssuranceLevel) assert.Empty(t, s.AuthenticatedAt) @@ -98,7 +104,7 @@ func TestSession(t *testing.T) { req.Header.Set("X-Forwarded-For", tc.input) s := session.NewInactiveSession() - require.NoError(t, s.Activate(req, &identity.Identity{State: identity.StateActive}, conf, authAt)) + require.NoError(t, reg.SessionManager().ActivateSession(req, s, &identity.Identity{NID: x.NewUUID(), State: identity.StateActive}, authAt)) assert.True(t, s.Active) assert.Equal(t, identity.NoAuthenticatorAssuranceLevel, s.AuthenticatorAssuranceLevel) assert.Equal(t, authAt, s.AuthenticatedAt) @@ -118,7 +124,7 @@ func TestSession(t *testing.T) { req.Header["X-Forwarded-For"] = []string{"54.155.246.232", "10.145.1.10"} s := session.NewInactiveSession() - require.NoError(t, s.Activate(req, &identity.Identity{State: identity.StateActive}, conf, authAt)) + require.NoError(t, reg.SessionManager().ActivateSession(req, s, &identity.Identity{NID: x.NewUUID(), State: identity.StateActive}, authAt)) assert.True(t, s.Active) assert.Equal(t, identity.NoAuthenticatorAssuranceLevel, s.AuthenticatorAssuranceLevel) assert.Equal(t, authAt, s.AuthenticatedAt) @@ -138,7 +144,7 @@ func TestSession(t *testing.T) { req.Header.Set("X-Forwarded-For", "217.73.188.139,162.158.203.149, 172.19.2.7") s := session.NewInactiveSession() - require.NoError(t, s.Activate(req, &identity.Identity{State: identity.StateActive}, conf, authAt)) + require.NoError(t, reg.SessionManager().ActivateSession(req, s, &identity.Identity{State: identity.StateActive, NID: x.NewUUID()}, authAt)) assert.True(t, s.Active) assert.Equal(t, identity.NoAuthenticatorAssuranceLevel, s.AuthenticatorAssuranceLevel) assert.Equal(t, authAt, s.AuthenticatedAt) @@ -159,7 +165,7 @@ func TestSession(t *testing.T) { req.Header.Set("Cf-Ipcountry", "Germany") s := session.NewInactiveSession() - require.NoError(t, s.Activate(req, &identity.Identity{State: identity.StateActive}, conf, authAt)) + require.NoError(t, reg.SessionManager().ActivateSession(req, s, &identity.Identity{NID: x.NewUUID(), State: identity.StateActive}, authAt)) assert.True(t, s.Active) assert.Equal(t, identity.NoAuthenticatorAssuranceLevel, s.AuthenticatorAssuranceLevel) assert.Equal(t, authAt, s.AuthenticatedAt) @@ -350,7 +356,9 @@ func TestSession(t *testing.T) { }) i := new(identity.Identity) i.State = identity.StateActive - s, _ := session.NewActiveSession(req, i, conf, authAt, identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + i.NID = x.NewUUID() + s, err := testhelpers.NewActiveSession(req, reg, i, authAt, identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + require.NoError(t, err) assert.False(t, s.CanBeRefreshed(ctx, conf), "fresh session is not refreshable") s.ExpiresAt = s.ExpiresAt.Add(-12 * time.Hour) diff --git a/session/test/persistence.go b/session/test/persistence.go index 727cc744bdce..d124aa23eb4f 100644 --- a/session/test/persistence.go +++ b/session/test/persistence.go @@ -8,6 +8,13 @@ import ( "testing" "time" + confighelpers "github.com/ory/kratos/driver/config/testhelpers" + + "github.com/pkg/errors" + "golang.org/x/sync/errgroup" + + "github.com/ory/x/dbal" + "github.com/gobuffalo/pop/v6" "github.com/ory/x/pagination/keysetpagination" @@ -37,8 +44,6 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { return func(t *testing.T) { _, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") - t.Run("case=not found", func(t *testing.T) { _, err := p.GetSession(ctx, x.NewUUID(), session.ExpandNothing) require.Error(t, err) @@ -604,5 +609,71 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface { _, err = p.GetSessionByToken(ctx, t2, session.ExpandNothing, identity.ExpandDefault) require.ErrorIs(t, err, sqlcon.ErrNoRows) }) + + t.Run("extend session lifespan but min time is not yet reached", func(t *testing.T) { + ctx := confighelpers.WithConfigValues(ctx, map[string]any{config.ViperKeySessionRefreshMinTimeLeft: 2 * time.Hour}) + + var expected session.Session + require.NoError(t, faker.FakeData(&expected)) + expected.ExpiresAt = time.Now().Add(time.Hour * 10).Round(time.Second).UTC() + require.NoError(t, p.CreateIdentity(ctx, expected.Identity)) + require.NoError(t, p.UpsertSession(ctx, &expected)) + + require.NoError(t, p.ExtendSession(ctx, expected.ID)) + actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing) + require.NoError(t, err) + assert.Equal(t, expected.ExpiresAt, actual.ExpiresAt) + }) + + t.Run("extend session lifespan", func(t *testing.T) { + ctx := confighelpers.WithConfigValues(ctx, map[string]any{config.ViperKeySessionRefreshMinTimeLeft: 2 * time.Hour}) + + var expected session.Session + require.NoError(t, faker.FakeData(&expected)) + expected.ExpiresAt = time.Now().Add(time.Hour).UTC() + require.NoError(t, p.CreateIdentity(ctx, expected.Identity)) + require.NoError(t, p.UpsertSession(ctx, &expected)) + + expectedExpiry := expected.Refresh(ctx, conf).ExpiresAt + require.NoError(t, p.ExtendSession(ctx, expected.ID)) + actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing) + require.NoError(t, err) + assert.GreaterOrEqual(t, 10*time.Second, expectedExpiry.Sub(actual.ExpiresAt).Abs()) + }) + + t.Run("extend session lifespan on CockroachDB", func(t *testing.T) { + if p.GetConnection(ctx).Dialect.Name() != dbal.DriverCockroachDB { + t.Skip("Skipping test because driver is not CockroachDB") + } + + ctx := confighelpers.WithConfigValue(ctx, config.ViperKeySessionRefreshMinTimeLeft, 2*time.Hour) + + var expected session.Session + require.NoError(t, faker.FakeData(&expected)) + expected.ExpiresAt = time.Now().Add(time.Hour).UTC() + require.NoError(t, p.CreateIdentity(ctx, expected.Identity)) + require.NoError(t, p.UpsertSession(ctx, &expected)) + + expectedExpiry := expected.Refresh(ctx, conf).ExpiresAt + + foundExpectedCockroachError := false + g := errgroup.Group{} + for range 10 { + g.Go(func() error { + err := p.ExtendSession(ctx, expected.ID) + if errors.Is(err, sqlcon.ErrNoRows) { + foundExpectedCockroachError = true + return nil + } + return err + }) + } + require.NoError(t, g.Wait()) + + actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing) + require.NoError(t, err) + assert.LessOrEqual(t, expectedExpiry.Sub(actual.ExpiresAt).Abs(), 10*time.Second) + assert.True(t, foundExpectedCockroachError, "We expect to find a not found error caused by ... FOR UPDATE SKIP LOCKED") + }) } } diff --git a/session/tokenizer_test.go b/session/tokenizer_test.go index fab54f09d99e..29a213f03142 100644 --- a/session/tokenizer_test.go +++ b/session/tokenizer_test.go @@ -10,10 +10,13 @@ import ( "testing" "time" + "github.com/golang-jwt/jwt/v5" + + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/herodot" "github.com/gofrs/uuid" - "github.com/golang-jwt/jwt/v5" "github.com/lestrrat-go/jwx/v2/jwk" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -68,6 +71,7 @@ func TestTokenizer(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "http://localhost/") + testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json") tkn := session.NewTokenizer(reg) nowDate := time.Date(2023, 02, 01, 00, 00, 00, 0, time.UTC) tkn.SetNowFunc(func() time.Time { @@ -77,8 +81,9 @@ func TestTokenizer(t *testing.T) { r := httptest.NewRequest("GET", "/sessions/whoami", nil) i := identity.NewIdentity("default") i.ID = uuid.FromStringOrNil("7458af86-c1d8-401c-978a-8da89133f78b") + i.NID = uuid.Must(uuid.NewV4()) - s, err := session.NewActiveSession(r, i, conf, now, identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) + s, err := testhelpers.NewActiveSession(r, reg, i, now, identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1) require.NoError(t, err) s.ID = uuid.FromStringOrNil("432caf86-c1d8-401c-978a-8da89133f78b") diff --git a/spec/api.json b/spec/api.json index 3ab71a4b9d53..a1c7060069ba 100644 --- a/spec/api.json +++ b/spec/api.json @@ -2,7 +2,7 @@ "components": { "responses": { "emptyResponse": { - "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 201." + "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 204." }, "identitySchemas": { "content": { @@ -81,6 +81,9 @@ } }, "schemas": { + "CodeChannel": { + "type": "string" + }, "DefaultError": {}, "Duration": { "description": "A Duration represents the elapsed time between two instants\nas an int64 nanosecond count. The representation limits the\nlargest representable duration to approximately 290 years.", @@ -119,6 +122,10 @@ }, "OAuth2Client": { "properties": { + "AdditionalProperties": { + "additionalProperties": {}, + "type": "object" + }, "access_token_strategy": { "description": "OAuth 2.0 Access Token Strategy AccessTokenStrategy is the strategy used to generate access tokens. Valid options are `jwt` and `opaque`. `jwt` is a bad idea, see https://www.ory.sh/docs/hydra/advanced#json-web-tokens Setting the stragegy here overrides the global setting in `strategies.access_token`.", "type": "string" @@ -333,6 +340,10 @@ "OAuth2ConsentRequestOpenIDConnectContext": { "description": "OAuth2ConsentRequestOpenIDConnectContext struct for OAuth2ConsentRequestOpenIDConnectContext", "properties": { + "AdditionalProperties": { + "additionalProperties": {}, + "type": "object" + }, "acr_values": { "description": "ACRValues is the Authentication AuthorizationContext Class Reference requested in the OAuth 2.0 Authorization request. It is a parameter defined by OpenID Connect and expresses which level of authentication (e.g. 2FA) is required. OpenID Connect defines it as follows: \u003e Requested Authentication AuthorizationContext Class Reference values. Space-separated string that specifies the acr values that the Authorization Server is being requested to use for processing this Authentication Request, with the values appearing in order of preference. The Authentication AuthorizationContext Class satisfied by the authentication performed is returned as the acr Claim Value, as specified in Section 2. The acr Claim is requested as a Voluntary Claim by this parameter.", "items": { @@ -369,6 +380,10 @@ "OAuth2LoginRequest": { "description": "OAuth2LoginRequest struct for OAuth2LoginRequest", "properties": { + "AdditionalProperties": { + "additionalProperties": {}, + "type": "object" + }, "challenge": { "description": "ID is the identifier (\\\"login challenge\\\") of the login request. It is used to identify the session.", "type": "string" @@ -465,6 +480,7 @@ "continueWith": { "discriminator": { "mapping": { + "redirect_browser_to": "#/components/schemas/continueWithRedirectBrowserTo", "set_ory_session_token": "#/components/schemas/continueWithSetOrySessionToken", "show_recovery_ui": "#/components/schemas/continueWithRecoveryUi", "show_settings_ui": "#/components/schemas/continueWithSettingsUi", @@ -484,6 +500,9 @@ }, { "$ref": "#/components/schemas/continueWithRecoveryUi" + }, + { + "$ref": "#/components/schemas/continueWithRedirectBrowserTo" } ] }, @@ -516,7 +535,7 @@ "type": "string" }, "url": { - "description": "The URL of the recovery flow", + "description": "The URL of the recovery flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.", "type": "string" } }, @@ -525,6 +544,28 @@ ], "type": "object" }, + "continueWithRedirectBrowserTo": { + "description": "Indicates, that the UI flow could be continued by showing a recovery ui", + "properties": { + "action": { + "description": "Action will always be `redirect_browser_to`\nredirect_browser_to ContinueWithActionRedirectBrowserToString", + "enum": [ + "redirect_browser_to" + ], + "type": "string", + "x-go-enum-desc": "redirect_browser_to ContinueWithActionRedirectBrowserToString" + }, + "redirect_browser_to": { + "description": "The URL to redirect the browser to", + "type": "string" + } + }, + "required": [ + "action", + "redirect_browser_to" + ], + "type": "object" + }, "continueWithSetOrySessionToken": { "description": "Indicates that a session was issued, and the application should use this token for authenticated requests", "properties": { @@ -574,6 +615,10 @@ "description": "The ID of the settings flow", "format": "uuid", "type": "string" + }, + "url": { + "description": "The URL of the settings flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.", + "type": "string" } }, "required": [ @@ -610,7 +655,7 @@ "type": "string" }, "url": { - "description": "The URL of the verification flow", + "description": "The URL of the verification flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.", "type": "string" }, "verifiable_address": { @@ -701,6 +746,9 @@ "pattern": "^([0-9]+(ns|us|ms|s|m|h))*$", "type": "string" }, + "flow_type": { + "$ref": "#/components/schemas/selfServiceFlowType" + }, "identity_id": { "description": "Identity to Recover\n\nThe identity's ID you wish to recover.", "format": "uuid", @@ -879,6 +927,7 @@ "type": "object" } }, + "title": "The not ready status of the service.", "type": "object" }, "healthStatus": { @@ -888,6 +937,7 @@ "type": "string" } }, + "title": "The health status of the service.", "type": "object" }, "identity": { @@ -992,7 +1042,7 @@ "type": "array" }, "type": { - "description": "Type discriminates between different types of credentials.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "Type discriminates between different types of credentials.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "enum": [ "password", "oidc", @@ -1000,11 +1050,13 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], "type": "string", - "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" }, "updated_at": { "description": "UpdatedAt is a helper struct field for gobuffalo.pop.", @@ -1022,12 +1074,23 @@ "identityCredentialsCode": { "description": "CredentialsCode represents a one time login/registration code", "properties": { - "address_type": { - "description": "The type of the address for this code", + "addresses": { + "items": { + "$ref": "#/components/schemas/identityCredentialsCodeAddress" + }, + "type": "array" + } + }, + "type": "object" + }, + "identityCredentialsCodeAddress": { + "properties": { + "address": { + "description": "The address for this code", "type": "string" }, - "used_at": { - "$ref": "#/components/schemas/NullTime" + "channel": { + "$ref": "#/components/schemas/CodeChannel" } }, "type": "object" @@ -1073,6 +1136,10 @@ "hashed_password": { "description": "HashedPassword is a hash-representation of the password.", "type": "string" + }, + "use_password_migration_hook": { + "description": "UsePasswordMigrationHook is set to true if the password should be migrated\nusing the password migration hook. If set, and the HashedPassword is empty, a\nwebhook will be called during login to migrate the password.", + "type": "boolean" } }, "title": "CredentialsPassword is contains the configuration for credentials of the type password.", @@ -1096,12 +1163,16 @@ "description": "Response for a single identity patch", "properties": { "action": { - "description": "The action for this specific patch\ncreate ActionCreate Create this identity.", + "description": "The action for this specific patch\ncreate ActionCreate Create this identity.\nerror ActionError Error indicates that the patch failed.", "enum": [ - "create" + "create", + "error" ], "type": "string", - "x-go-enum-desc": "create ActionCreate Create this identity." + "x-go-enum-desc": "create ActionCreate Create this identity.\nerror ActionError Error indicates that the patch failed." + }, + "error": { + "$ref": "#/components/schemas/DefaultError" }, "identity": { "description": "The identity ID payload of this patch", @@ -1221,6 +1292,10 @@ "password": { "description": "The password in plain text if no hash is available.", "type": "string" + }, + "use_password_migration_hook": { + "description": "If set to true, the password will be migrated using the password migration hook.", + "type": "boolean" } }, "type": "object" @@ -1265,7 +1340,7 @@ "description": "This object represents a login flow. A login flow is initiated at the \"Initiate Login API / Browser Flow\"\nendpoint by a client.\n\nOnce a login flow is completed successfully, a session cookie or session token will be issued.", "properties": { "active": { - "description": "The active login method\n\nIf set contains the login method used. If the flow is new, it is unset.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "The active login method\n\nIf set contains the login method used. If the flow is new, it is unset.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "enum": [ "password", "oidc", @@ -1273,11 +1348,13 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], "type": "string", - "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" }, "created_at": { "description": "CreatedAt is a helper struct field for gobuffalo.pop.", @@ -1331,6 +1408,10 @@ "state": { "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method to sign in with\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the login challenge was passed." }, + "transient_payload": { + "description": "TransientPayload is used to pass data from the login to hooks and email templates", + "type": "object" + }, "type": { "$ref": "#/components/schemas/selfServiceFlowType" }, @@ -1356,13 +1437,14 @@ "type": "object" }, "loginFlowState": { - "description": "The state represents the state of the login flow.\n\nchoose_method: ask the user to choose a method (e.g. login account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the login challenge was passed.", + "description": "The experimental state represents the state of a login flow. This field is EXPERIMENTAL and subject to change!", "enum": [ "choose_method", "sent_email", "passed_challenge" ], - "title": "Login Flow State" + "title": "Login flow state (experimental)", + "type": "string" }, "logoutFlow": { "description": "Logout Flow", @@ -1629,6 +1711,10 @@ "state": { "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed." }, + "transient_payload": { + "description": "TransientPayload is used to pass data from the recovery flow to hooks and email templates", + "type": "object" + }, "type": { "$ref": "#/components/schemas/selfServiceFlowType" }, @@ -1649,13 +1735,13 @@ "type": "object" }, "recoveryFlowState": { - "description": "The state represents the state of the recovery flow.\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed.", + "description": "The experimental state represents the state of a recovery flow. This field is EXPERIMENTAL and subject to change!", "enum": [ "choose_method", "sent_email", "passed_challenge" ], - "title": "Recovery Flow State" + "title": "Recovery flow state (experimental)" }, "recoveryIdentityAddress": { "properties": { @@ -1709,7 +1795,7 @@ "registrationFlow": { "properties": { "active": { - "description": "Active, if set, contains the registration method that is being used. It is initially\nnot set.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "Active, if set, contains the registration method that is being used. It is initially\nnot set.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "enum": [ "password", "oidc", @@ -1717,11 +1803,13 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], "type": "string", - "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" }, "expires_at": { "description": "ExpiresAt is the time (UTC) when the flow expires. If the user still wishes to log in,\na new flow has to be initiated.", @@ -1786,13 +1874,14 @@ "type": "object" }, "registrationFlowState": { - "description": "choose_method: ask the user to choose a method (e.g. registration with email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the registration challenge was passed.", + "description": "The experimental state represents the state of a registration flow. This field is EXPERIMENTAL and subject to change!", "enum": [ "choose_method", "sent_email", "passed_challenge" ], - "title": "State represents the state of this request:" + "title": "Registration flow state (experimental)", + "type": "string" }, "selfServiceFlowExpiredError": { "description": "Is sent when a flow is expired", @@ -1989,6 +2078,10 @@ "state": { "description": "State represents the state of this flow. It knows two states:\n\nshow_form: No user data has been collected, or it is invalid, and thus the form should be shown.\nsuccess: Indicates that the settings flow has been updated successfully with the provided data.\nDone will stay true when repeatedly checking. If set to true, done will revert back to false only\nwhen a flow with invalid (e.g. \"please use a valid phone number\") data was sent." }, + "transient_payload": { + "description": "TransientPayload is used to pass data from the settings flow to hooks and email templates", + "type": "object" + }, "type": { "$ref": "#/components/schemas/selfServiceFlowType" }, @@ -2010,12 +2103,13 @@ "type": "object" }, "settingsFlowState": { - "description": "show_form: No user data has been collected, or it is invalid, and thus the form should be shown.\nsuccess: Indicates that the settings flow has been updated successfully with the provided data.\nDone will stay true when repeatedly checking. If set to true, done will revert back to false only\nwhen a flow with invalid (e.g. \"please use a valid phone number\") data was sent.", + "description": "The experimental state represents the state of a settings flow. This field is EXPERIMENTAL and subject to change!", "enum": [ "show_form", "success" ], - "title": "State represents the state of this flow. It knows two states:" + "title": "Settings flow state (experimental)", + "type": "string" }, "successfulAdminIdentitySession": { "properties": { @@ -2055,6 +2149,13 @@ "successfulNativeLogin": { "description": "The Response for Login Flows via API", "properties": { + "continue_with": { + "description": "Contains a list of actions, that could follow this flow\n\nIt can, for example, this will contain a reference to the verification flow, created as part of the user's\nregistration or the token of the session.", + "items": { + "$ref": "#/components/schemas/continueWith" + }, + "type": "array" + }, "session": { "$ref": "#/components/schemas/session" }, @@ -2158,7 +2259,7 @@ "$ref": "#/components/schemas/uiNodeAttributes" }, "group": { - "description": "Group specifies which group (e.g. password authenticator) this node belongs to.\ndefault DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup", + "description": "Group specifies which group (e.g. password authenticator) this node belongs to.\ndefault DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup\nidentifier_first IdentifierFirstGroup", "enum": [ "default", "password", @@ -2168,10 +2269,12 @@ "code", "totp", "lookup_secret", - "webauthn" + "webauthn", + "passkey", + "identifier_first" ], "type": "string", - "x-go-enum-desc": "default DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup" + "x-go-enum-desc": "default DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup\nidentifier_first IdentifierFirstGroup" }, "messages": { "$ref": "#/components/schemas/uiTexts" @@ -2213,8 +2316,16 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"a\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"a\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "type": "string", + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "title": { "$ref": "#/components/schemas/uiText" @@ -2271,8 +2382,16 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"img\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"img\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "type": "string", + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "src": { "description": "The image's source URL.\n\nformat: uri", @@ -2318,8 +2437,8 @@ "$ref": "#/components/schemas/uiText" }, "maxlength": { - "description": "The maxlength attribute for the input.", - "format": "uint64", + "description": "MaxLength may contain the input's maximum length.", + "format": "int64", "type": "integer" }, "minlength": { @@ -2332,13 +2451,51 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "type": "string", + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "onclick": { - "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.", + "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead.", "type": "string" }, + "onclickTrigger": { + "description": "OnClickTrigger may contain a WebAuthn trigger which should be executed on click.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration", + "enum": [ + "oryWebAuthnRegistration", + "oryWebAuthnLogin", + "oryPasskeyLogin", + "oryPasskeyLoginAutocompleteInit", + "oryPasskeyRegistration", + "oryPasskeySettingsRegistration" + ], + "type": "string", + "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration" + }, + "onload": { + "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead.", + "type": "string" + }, + "onloadTrigger": { + "description": "OnLoadTrigger may contain a WebAuthn trigger which should be executed on load.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration", + "enum": [ + "oryWebAuthnRegistration", + "oryWebAuthnLogin", + "oryPasskeyLogin", + "oryPasskeyLoginAutocompleteInit", + "oryPasskeyRegistration", + "oryPasskeySettingsRegistration" + ], + "type": "string", + "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration" + }, "pattern": { "description": "The input's pattern.", "type": "string" @@ -2408,8 +2565,16 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"script\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"script\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "type": "string", + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "nonce": { "description": "Nonce for CSP\n\nA nonce you may want to use to improve your Content Security Policy.\nYou do not have to use this value but if you want to improve your CSP\npolicies you may use it. You can also choose to use your own nonce value!", @@ -2449,8 +2614,16 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"text\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"text\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "type": "string", + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "text": { "$ref": "#/components/schemas/uiText" @@ -2551,8 +2724,10 @@ "discriminator": { "mapping": { "code": "#/components/schemas/updateLoginFlowWithCodeMethod", + "identifier_first": "#/components/schemas/updateLoginFlowWithIdentifierFirstMethod", "lookup_secret": "#/components/schemas/updateLoginFlowWithLookupSecretMethod", "oidc": "#/components/schemas/updateLoginFlowWithOidcMethod", + "passkey": "#/components/schemas/updateLoginFlowWithPasskeyMethod", "password": "#/components/schemas/updateLoginFlowWithPasswordMethod", "totp": "#/components/schemas/updateLoginFlowWithTotpMethod", "webauthn": "#/components/schemas/updateLoginFlowWithWebAuthnMethod" @@ -2577,12 +2752,22 @@ }, { "$ref": "#/components/schemas/updateLoginFlowWithCodeMethod" + }, + { + "$ref": "#/components/schemas/updateLoginFlowWithPasskeyMethod" + }, + { + "$ref": "#/components/schemas/updateLoginFlowWithIdentifierFirstMethod" } ] }, "updateLoginFlowWithCodeMethod": { "description": "Update Login flow using the code method", "properties": { + "address": { + "description": "Address is the address to send the code to, in case that there are multiple addresses. This field\nis only used in two-factor flows and is ineffective for passwordless flows.", + "type": "string" + }, "code": { "description": "Code is the 6 digits code sent to the user", "type": "string" @@ -2602,6 +2787,10 @@ "resend": { "description": "Resend is set when the user wants to resend the code", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -2610,6 +2799,32 @@ ], "type": "object" }, + "updateLoginFlowWithIdentifierFirstMethod": { + "description": "Update Login Flow with Multi-Step Method", + "properties": { + "csrf_token": { + "description": "Sending the anti-csrf token is only required for browser login flows.", + "type": "string" + }, + "identifier": { + "description": "Identifier is the email or username of the user trying to log in.", + "type": "string" + }, + "method": { + "description": "Method should be set to \"password\" when logging in using the identifier and password strategy.", + "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + } + }, + "required": [ + "method", + "identifier" + ], + "type": "object" + }, "updateLoginFlowWithLookupSecretMethod": { "description": "Update Login Flow with Lookup Secret Method", "properties": { @@ -2640,7 +2855,7 @@ "type": "string" }, "id_token": { - "description": "IDToken is an optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", + "description": "IDToken is an optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple\nGoogle", "type": "string" }, "id_token_nonce": { @@ -2659,6 +2874,10 @@ "description": "The identity traits. This is a placeholder for the registration flow.", "type": "object" }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + }, "upstream_parameters": { "description": "UpstreamParameters are the parameters that are passed to the upstream identity provider.\n\nThese parameters are optional and depend on what the upstream identity provider supports.\nSupported parameters are:\n`login_hint` (string): The `login_hint` parameter suppresses the account chooser and either pre-fills the email box on the sign-in form, or selects the proper session.\n`hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`.\n`prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`.", "type": "object" @@ -2670,6 +2889,27 @@ ], "type": "object" }, + "updateLoginFlowWithPasskeyMethod": { + "description": "Update Login Flow with Passkey Method", + "properties": { + "csrf_token": { + "description": "Sending the anti-csrf token is only required for browser login flows.", + "type": "string" + }, + "method": { + "description": "Method should be set to \"passkey\" when logging in using the Passkey strategy.", + "type": "string" + }, + "passkey_login": { + "description": "Login a WebAuthn Security Key\n\nThis must contain the ID of the WebAuthN connection.", + "type": "string" + } + }, + "required": [ + "method" + ], + "type": "object" + }, "updateLoginFlowWithPasswordMethod": { "description": "Update Login Flow with Password Method", "properties": { @@ -2692,6 +2932,10 @@ "password_identifier": { "description": "Identifier is the email or username of the user trying to log in.\nThis field is deprecated!", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -2715,6 +2959,10 @@ "totp_code": { "description": "The TOTP code.", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -2738,6 +2986,10 @@ "description": "Method should be set to \"webAuthn\" when logging in using the WebAuthn strategy.", "type": "string" }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + }, "webauthn_login": { "description": "Login a WebAuthn Security Key\n\nThis must contain the ID of the WebAuthN connection.", "type": "string" @@ -2790,6 +3042,10 @@ ], "type": "string", "x-go-enum-desc": "link RecoveryStrategyLink\ncode RecoveryStrategyCode" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -2816,6 +3072,10 @@ ], "type": "string", "x-go-enum-desc": "link RecoveryStrategyLink\ncode RecoveryStrategyCode" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -2830,7 +3090,9 @@ "mapping": { "code": "#/components/schemas/updateRegistrationFlowWithCodeMethod", "oidc": "#/components/schemas/updateRegistrationFlowWithOidcMethod", + "passkey": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod", "password": "#/components/schemas/updateRegistrationFlowWithPasswordMethod", + "profile": "#/components/schemas/updateRegistrationFlowWithProfileMethod", "webauthn": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod" }, "propertyName": "method" @@ -2847,6 +3109,12 @@ }, { "$ref": "#/components/schemas/updateRegistrationFlowWithCodeMethod" + }, + { + "$ref": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod" + }, + { + "$ref": "#/components/schemas/updateRegistrationFlowWithProfileMethod" } ] }, @@ -2892,7 +3160,7 @@ "type": "string" }, "id_token": { - "description": "IDToken is an optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", + "description": "IDToken is an optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple\nGoogle", "type": "string" }, "id_token_nonce": { @@ -2926,6 +3194,36 @@ ], "type": "object" }, + "updateRegistrationFlowWithPasskeyMethod": { + "description": "Update Registration Flow with Passkey Method", + "properties": { + "csrf_token": { + "description": "CSRFToken is the anti-CSRF token", + "type": "string" + }, + "method": { + "description": "Method\n\nShould be set to \"passkey\" when trying to add, update, or remove a Passkey.", + "type": "string" + }, + "passkey_register": { + "description": "Register a WebAuthn Security Key\n\nIt is expected that the JSON returned by the WebAuthn registration process\nis included here.", + "type": "string" + }, + "traits": { + "description": "The identity's traits", + "type": "object" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + } + }, + "required": [ + "traits", + "method" + ], + "type": "object" + }, "updateRegistrationFlowWithPasswordMethod": { "description": "Update Registration Flow with Password Method", "properties": { @@ -2957,6 +3255,41 @@ ], "type": "object" }, + "updateRegistrationFlowWithProfileMethod": { + "description": "Update Registration Flow with Profile Method", + "properties": { + "csrf_token": { + "description": "The Anti-CSRF Token\n\nThis token is only required when performing browser flows.", + "type": "string" + }, + "method": { + "description": "Method\n\nShould be set to profile when trying to update a profile.", + "type": "string" + }, + "screen": { + "description": "Screen requests navigation to a previous screen.\n\nThis must be set to credential-selection to go back to the credential\nselection screen.\ncredential-selection RegistrationScreenCredentialSelection nolint:gosec // not a credential\nprevious RegistrationScreenPrevious", + "enum": [ + "credential-selection", + "previous" + ], + "type": "string", + "x-go-enum-desc": "credential-selection RegistrationScreenCredentialSelection nolint:gosec // not a credential\nprevious RegistrationScreenPrevious" + }, + "traits": { + "description": "Traits\n\nThe identity's traits.", + "type": "object" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + } + }, + "required": [ + "traits", + "method" + ], + "type": "object" + }, "updateRegistrationFlowWithWebAuthnMethod": { "description": "Update Registration Flow with WebAuthn Method", "properties": { @@ -2997,6 +3330,7 @@ "mapping": { "lookup_secret": "#/components/schemas/updateSettingsFlowWithLookupMethod", "oidc": "#/components/schemas/updateSettingsFlowWithOidcMethod", + "passkey": "#/components/schemas/updateSettingsFlowWithPasskeyMethod", "password": "#/components/schemas/updateSettingsFlowWithPasswordMethod", "profile": "#/components/schemas/updateSettingsFlowWithProfileMethod", "totp": "#/components/schemas/updateSettingsFlowWithTotpMethod", @@ -3014,9 +3348,6 @@ { "$ref": "#/components/schemas/updateSettingsFlowWithOidcMethod" }, - { - "$ref": "#/components/schemas/updateSettingsFlowWithOidcMethod" - }, { "$ref": "#/components/schemas/updateSettingsFlowWithTotpMethod" }, @@ -3025,6 +3356,9 @@ }, { "$ref": "#/components/schemas/updateSettingsFlowWithLookupMethod" + }, + { + "$ref": "#/components/schemas/updateSettingsFlowWithPasskeyMethod" } ] }, @@ -3054,6 +3388,10 @@ "method": { "description": "Method\n\nShould be set to \"lookup\" when trying to add, update, or remove a lookup pairing.", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -3080,6 +3418,10 @@ "description": "The identity's traits\n\nin: body", "type": "object" }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + }, "unlink": { "description": "Unlink this provider\n\nEither this or `link` must be set.\n\ntype: string\nin: body", "type": "string" @@ -3094,6 +3436,31 @@ ], "type": "object" }, + "updateSettingsFlowWithPasskeyMethod": { + "description": "Update Settings Flow with Passkey Method", + "properties": { + "csrf_token": { + "description": "CSRFToken is the anti-CSRF token", + "type": "string" + }, + "method": { + "description": "Method\n\nShould be set to \"passkey\" when trying to add, update, or remove a webAuthn pairing.", + "type": "string" + }, + "passkey_remove": { + "description": "Remove a WebAuthn Security Key\n\nThis must contain the ID of the WebAuthN connection.", + "type": "string" + }, + "passkey_settings_register": { + "description": "Register a WebAuthn Security Key\n\nIt is expected that the JSON returned by the WebAuthn registration process\nis included here.", + "type": "string" + } + }, + "required": [ + "method" + ], + "type": "object" + }, "updateSettingsFlowWithPasswordMethod": { "description": "Update Settings Flow with Password Method", "properties": { @@ -3108,6 +3475,10 @@ "password": { "description": "Password is the updated password", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -3130,6 +3501,10 @@ "traits": { "description": "Traits\n\nThe identity's traits.", "type": "object" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -3156,6 +3531,10 @@ "totp_unlink": { "description": "UnlinkTOTP if true will remove the TOTP pairing,\neffectively removing the credential. This can be used\nto set up a new TOTP device.", "type": "boolean" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -3174,6 +3553,10 @@ "description": "Method\n\nShould be set to \"webauthn\" when trying to add, update, or remove a webAuthn pairing.", "type": "string" }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + }, "webauthn_register": { "description": "Register a WebAuthn Security Key\n\nIt is expected that the JSON returned by the WebAuthn registration process\nis included here.", "type": "string" @@ -3232,6 +3615,10 @@ ], "type": "string", "x-go-enum-desc": "link VerificationStrategyLink\ncode VerificationStrategyCode" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -3258,6 +3645,10 @@ ], "type": "string", "x-go-enum-desc": "link VerificationStrategyLink\ncode VerificationStrategyCode" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } }, "required": [ @@ -3352,6 +3743,10 @@ "state": { "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. verify your email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the verification challenge was passed." }, + "transient_payload": { + "description": "TransientPayload is used to pass data from the verification flow to hooks and email templates", + "type": "object" + }, "type": { "$ref": "#/components/schemas/selfServiceFlowType" }, @@ -3369,13 +3764,13 @@ "type": "object" }, "verificationFlowState": { - "description": "The state represents the state of the verification flow.\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed.", + "description": "The experimental state represents the state of a verification flow. This field is EXPERIMENTAL and subject to change!", "enum": [ "choose_method", "sent_email", "passed_challenge" ], - "title": "Verification Flow State" + "title": "Verification flow state (experimental)" }, "version": { "properties": { @@ -3657,6 +4052,17 @@ "schema": { "type": "string" } + }, + { + "description": "Include Credentials in Response\n\nInclude any credential, for example `password` or `oidc`, in the response. When set to `oidc`, This will return\nthe initial OAuth 2.0 Access Token, OAuth 2.0 Refresh Token and the OpenID Connect ID Token if available.", + "in": "query", + "name": "include_credential", + "schema": { + "items": { + "type": "string" + }, + "type": "array" + } } ], "responses": { @@ -3685,7 +4091,7 @@ ] }, "patch": { - "description": "Creates or delete multiple\n[identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model).\nThis endpoint can also be used to [import\ncredentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities)\nfor instance passwords, social sign in configurations or multifactor methods.", + "description": "Creates multiple\n[identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model).\nThis endpoint can also be used to [import\ncredentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities)\nfor instance passwords, social sign in configurations or multifactor methods.", "operationId": "batchPatchIdentities", "requestBody": { "content": { @@ -3744,7 +4150,7 @@ "oryAccessToken": [] } ], - "summary": "Create and deletes multiple identities", + "summary": "Create multiple identities", "tags": [ "identity" ] @@ -3891,6 +4297,8 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], @@ -4117,7 +4525,7 @@ }, "/admin/identities/{id}/credentials/{type}": { "delete": { - "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type\nYou can only delete second factor (aal2) credentials.", + "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type.\nYou cannot delete password or code auth credentials through this API.", "operationId": "deleteIdentityCredentials", "parameters": [ { @@ -4130,7 +4538,7 @@ } }, { - "description": "Type is the type of credentials to be deleted.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "Type is the type of credentials to delete.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "in": "path", "name": "type", "required": true, @@ -4142,12 +4550,22 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], "type": "string" }, - "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + }, + { + "description": "Identifier is the identifier of the OIDC credential to delete.\nFind the identifier by calling the `GET /admin/identities/{id}?include_credential=oidc` endpoint.", + "in": "query", + "name": "identifier", + "schema": { + "type": "string" + } } ], "responses": { @@ -4614,11 +5032,11 @@ "in": "query", "name": "expand", "schema": { - "enum": [ - "identity", - "devices" - ], "items": { + "enum": [ + "identity", + "devices" + ], "type": "string" }, "type": "array" @@ -4730,11 +5148,11 @@ "in": "query", "name": "expand", "schema": { - "enum": [ - "identity", - "devices" - ], "items": { + "enum": [ + "identity", + "devices" + ], "type": "string" }, "type": "array" @@ -4795,7 +5213,7 @@ }, "/admin/sessions/{id}/extend": { "patch": { - "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.", + "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nThis endpoint ignores consecutive requests to extend the same session and returns a 404 error in those\nscenarios. This endpoint also returns 404 errors if the session does not exist.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.", "operationId": "extendSession", "parameters": [ { @@ -4819,6 +5237,9 @@ }, "description": "session" }, + "204": { + "$ref": "#/components/responses/emptyResponse" + }, "400": { "content": { "application/json": { @@ -5440,7 +5861,7 @@ } }, { - "description": "Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows.", + "description": "Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows.\n\nDEPRECATED: This field is deprecated. Please remove it from your requests. The user will now see a choice\nof MFA credentials to choose from to perform the second factor instead.", "in": "query", "name": "via", "schema": { @@ -5538,6 +5959,14 @@ "schema": { "type": "string" } + }, + { + "description": "Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows.\n\nDEPRECATED: This field is deprecated. Please remove it from your requests. The user will now see a choice\nof MFA credentials to choose from to perform the second factor instead.", + "in": "query", + "name": "via", + "schema": { + "type": "string" + } } ], "responses": { @@ -7499,4 +7928,4 @@ ], "x-forwarded-proto": "string", "x-request-id": "string" -} \ No newline at end of file +} diff --git a/spec/swagger.json b/spec/swagger.json index d6feba914f27..1a3ce5dade32 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -252,6 +252,15 @@ "description": "This is an EXPERIMENTAL parameter that WILL CHANGE. Do NOT rely on consistent, deterministic behavior.\nTHIS PARAMETER WILL BE REMOVED IN AN UPCOMING RELEASE WITHOUT ANY MIGRATION PATH.\n\nCredentialsIdentifierSimilar is the (partial) identifier (username, email) of the credentials to look up using similarity search.\nOnly one of CredentialsIdentifier and CredentialsIdentifierSimilar can be used.", "name": "preview_credentials_identifier_similar", "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "description": "Include Credentials in Response\n\nInclude any credential, for example `password` or `oidc`, in the response. When set to `oidc`, This will return\nthe initial OAuth 2.0 Access Token, OAuth 2.0 Refresh Token and the OpenID Connect ID Token if available.", + "name": "include_credential", + "in": "query" } ], "responses": { @@ -330,7 +339,7 @@ "oryAccessToken": [] } ], - "description": "Creates or delete multiple\n[identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model).\nThis endpoint can also be used to [import\ncredentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities)\nfor instance passwords, social sign in configurations or multifactor methods.", + "description": "Creates multiple\n[identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model).\nThis endpoint can also be used to [import\ncredentials](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities)\nfor instance passwords, social sign in configurations or multifactor methods.", "consumes": [ "application/json" ], @@ -344,7 +353,7 @@ "tags": [ "identity" ], - "summary": "Create and deletes multiple identities", + "summary": "Create multiple identities", "operationId": "batchPatchIdentities", "parameters": [ { @@ -424,6 +433,8 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], @@ -651,7 +662,7 @@ "oryAccessToken": [] } ], - "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type\nYou can only delete second factor (aal2) credentials.", + "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type.\nYou cannot delete password or code auth credentials through this API.", "consumes": [ "application/json" ], @@ -683,15 +694,23 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], "type": "string", - "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", - "description": "Type is the type of credentials to be deleted.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "Type is the type of credentials to delete.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "name": "type", "in": "path", "required": true + }, + { + "type": "string", + "description": "Identifier is the identifier of the OIDC credential to delete.\nFind the identifier by calling the `GET /admin/identities/{id}?include_credential=oidc` endpoint.", + "name": "identifier", + "in": "query" } ], "responses": { @@ -1080,12 +1099,12 @@ "in": "query" }, { - "enum": [ - "identity", - "devices" - ], "type": "array", "items": { + "enum": [ + "identity", + "devices" + ], "type": "string" }, "description": "ExpandOptions is a query parameter encoded list of all properties that must be expanded in the Session.\nIf no value is provided, the expandable properties are skipped.", @@ -1131,12 +1150,12 @@ "operationId": "getSession", "parameters": [ { - "enum": [ - "identity", - "devices" - ], "type": "array", "items": { + "enum": [ + "identity", + "devices" + ], "type": "string" }, "description": "ExpandOptions is a query parameter encoded list of all properties that must be expanded in the Session.\nExample - ?expand=Identity\u0026expand=Devices\nIf no value is provided, the expandable properties are skipped.", @@ -1229,7 +1248,7 @@ "oryAccessToken": [] } ], - "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.", + "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nThis endpoint ignores consecutive requests to extend the same session and returns a 404 error in those\nscenarios. This endpoint also returns 404 errors if the session does not exist.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.", "schemes": [ "http", "https" @@ -1255,6 +1274,9 @@ "$ref": "#/definitions/session" } }, + "204": { + "$ref": "#/responses/emptyResponse" + }, "400": { "description": "errorGeneric", "schema": { @@ -1758,7 +1780,7 @@ }, { "type": "string", - "description": "Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows.", + "description": "Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows.\n\nDEPRECATED: This field is deprecated. Please remove it from your requests. The user will now see a choice\nof MFA credentials to choose from to perform the second factor instead.", "name": "via", "in": "query" } @@ -1836,6 +1858,12 @@ "description": "An optional organization ID that should be used for logging this user in.\nThis parameter is only effective in the Ory Network.", "name": "organization", "in": "query" + }, + { + "type": "string", + "description": "Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows.\n\nDEPRECATED: This field is deprecated. Please remove it from your requests. The user will now see a choice\nof MFA credentials to choose from to perform the second factor instead.", + "name": "via", + "in": "query" } ], "responses": { @@ -3393,6 +3421,9 @@ } }, "definitions": { + "CodeChannel": { + "type": "string" + }, "DefaultError": {}, "Duration": { "description": "A Duration represents the elapsed time between two instants\nas an int64 nanosecond count. The representation limits the\nlargest representable duration to approximately 290 years.", @@ -3407,20 +3438,6 @@ "type": "object", "title": "JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger." }, - "NullTime": { - "description": "NullTime implements the [Scanner] interface so\nit can be used as a scan destination, similar to [NullString].", - "type": "object", - "title": "NullTime represents a [time.Time] that may be null.", - "properties": { - "Time": { - "type": "string", - "format": "date-time" - }, - "Valid": { - "type": "boolean" - } - } - }, "NullUUID": { "description": "NullUUID can be used with the standard sql package to represent a\nUUID value that can be NULL in the database.", "type": "object", @@ -3438,6 +3455,10 @@ "type": "object", "title": "OAuth2Client OAuth 2.0 Clients are used to perform OAuth 2.0 and OpenID Connect flows. Usually, OAuth 2.0 clients are generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities.", "properties": { + "AdditionalProperties": { + "type": "object", + "additionalProperties": {} + }, "access_token_strategy": { "description": "OAuth 2.0 Access Token Strategy AccessTokenStrategy is the strategy used to generate access tokens. Valid options are `jwt` and `opaque`. `jwt` is a bad idea, see https://www.ory.sh/docs/hydra/advanced#json-web-tokens Setting the stragegy here overrides the global setting in `strategies.access_token`.", "type": "string" @@ -3652,6 +3673,10 @@ "description": "OAuth2ConsentRequestOpenIDConnectContext struct for OAuth2ConsentRequestOpenIDConnectContext", "type": "object", "properties": { + "AdditionalProperties": { + "type": "object", + "additionalProperties": {} + }, "acr_values": { "description": "ACRValues is the Authentication AuthorizationContext Class Reference requested in the OAuth 2.0 Authorization request. It is a parameter defined by OpenID Connect and expresses which level of authentication (e.g. 2FA) is required. OpenID Connect defines it as follows: \u003e Requested Authentication AuthorizationContext Class Reference values. Space-separated string that specifies the acr values that the Authorization Server is being requested to use for processing this Authentication Request, with the values appearing in order of preference. The Authentication AuthorizationContext Class satisfied by the authentication performed is returned as the acr Claim Value, as specified in Section 2. The acr Claim is requested as a Voluntary Claim by this parameter.", "type": "array", @@ -3688,6 +3713,10 @@ "description": "OAuth2LoginRequest struct for OAuth2LoginRequest", "type": "object", "properties": { + "AdditionalProperties": { + "type": "object", + "additionalProperties": {} + }, "challenge": { "description": "ID is the identifier (\\\"login challenge\\\") of the login request. It is used to identify the session.", "type": "string" @@ -3802,7 +3831,29 @@ "format": "uuid" }, "url": { - "description": "The URL of the recovery flow", + "description": "The URL of the recovery flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.", + "type": "string" + } + } + }, + "continueWithRedirectBrowserTo": { + "description": "Indicates, that the UI flow could be continued by showing a recovery ui", + "type": "object", + "required": [ + "action", + "redirect_browser_to" + ], + "properties": { + "action": { + "description": "Action will always be `redirect_browser_to`\nredirect_browser_to ContinueWithActionRedirectBrowserToString", + "type": "string", + "enum": [ + "redirect_browser_to" + ], + "x-go-enum-desc": "redirect_browser_to ContinueWithActionRedirectBrowserToString" + }, + "redirect_browser_to": { + "description": "The URL to redirect the browser to", "type": "string" } } @@ -3860,6 +3911,10 @@ "description": "The ID of the settings flow", "type": "string", "format": "uuid" + }, + "url": { + "description": "The URL of the settings flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.", + "type": "string" } } }, @@ -3897,7 +3952,7 @@ "format": "uuid" }, "url": { - "description": "The URL of the verification flow", + "description": "The URL of the verification flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.", "type": "string" }, "verifiable_address": { @@ -3981,6 +4036,9 @@ "type": "string", "pattern": "^([0-9]+(ns|us|ms|s|m|h))*$" }, + "flow_type": { + "$ref": "#/definitions/selfServiceFlowType" + }, "identity_id": { "description": "Identity to Recover\n\nThe identity's ID you wish to recover.", "type": "string", @@ -4148,6 +4206,7 @@ }, "healthNotReadyStatus": { "type": "object", + "title": "The not ready status of the service.", "properties": { "errors": { "description": "Errors contains a list of errors that caused the not ready status.", @@ -4160,6 +4219,7 @@ }, "healthStatus": { "type": "object", + "title": "The health status of the service.", "properties": { "status": { "description": "Status always contains \"ok\".", @@ -4270,7 +4330,7 @@ } }, "type": { - "description": "Type discriminates between different types of credentials.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "Type discriminates between different types of credentials.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "type": "string", "enum": [ "password", @@ -4279,10 +4339,12 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], - "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" }, "updated_at": { "description": "UpdatedAt is a helper struct field for gobuffalo.pop.", @@ -4300,12 +4362,23 @@ "description": "CredentialsCode represents a one time login/registration code", "type": "object", "properties": { - "address_type": { - "description": "The type of the address for this code", + "addresses": { + "type": "array", + "items": { + "$ref": "#/definitions/identityCredentialsCodeAddress" + } + } + } + }, + "identityCredentialsCodeAddress": { + "type": "object", + "properties": { + "address": { + "description": "The address for this code", "type": "string" }, - "used_at": { - "$ref": "#/definitions/NullTime" + "channel": { + "$ref": "#/definitions/CodeChannel" } } }, @@ -4352,6 +4425,10 @@ "hashed_password": { "description": "HashedPassword is a hash-representation of the password.", "type": "string" + }, + "use_password_migration_hook": { + "description": "UsePasswordMigrationHook is set to true if the password should be migrated\nusing the password migration hook. If set, and the HashedPassword is empty, a\nwebhook will be called during login to migrate the password.", + "type": "boolean" } } }, @@ -4374,12 +4451,16 @@ "type": "object", "properties": { "action": { - "description": "The action for this specific patch\ncreate ActionCreate Create this identity.", + "description": "The action for this specific patch\ncreate ActionCreate Create this identity.\nerror ActionError Error indicates that the patch failed.", "type": "string", "enum": [ - "create" + "create", + "error" ], - "x-go-enum-desc": "create ActionCreate Create this identity." + "x-go-enum-desc": "create ActionCreate Create this identity.\nerror ActionError Error indicates that the patch failed." + }, + "error": { + "$ref": "#/definitions/DefaultError" }, "identity": { "description": "The identity ID payload of this patch", @@ -4500,6 +4581,10 @@ "password": { "description": "The password in plain text if no hash is available.", "type": "string" + }, + "use_password_migration_hook": { + "description": "If set to true, the password will be migrated using the password migration hook.", + "type": "boolean" } } }, @@ -4554,7 +4639,7 @@ ], "properties": { "active": { - "description": "The active login method\n\nIf set contains the login method used. If the flow is new, it is unset.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "The active login method\n\nIf set contains the login method used. If the flow is new, it is unset.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "type": "string", "enum": [ "password", @@ -4563,10 +4648,12 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], - "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" }, "created_at": { "description": "CreatedAt is a helper struct field for gobuffalo.pop.", @@ -4620,6 +4707,10 @@ "state": { "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method to sign in with\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the login challenge was passed." }, + "transient_payload": { + "description": "TransientPayload is used to pass data from the login to hooks and email templates", + "type": "object" + }, "type": { "$ref": "#/definitions/selfServiceFlowType" }, @@ -4633,10 +4724,6 @@ } } }, - "loginFlowState": { - "description": "The state represents the state of the login flow.\n\nchoose_method: ask the user to choose a method (e.g. login account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the login challenge was passed.", - "title": "Login Flow State" - }, "logoutFlow": { "description": "Logout Flow", "type": "object", @@ -4904,6 +4991,10 @@ "state": { "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed." }, + "transient_payload": { + "description": "TransientPayload is used to pass data from the recovery flow to hooks and email templates", + "type": "object" + }, "type": { "$ref": "#/definitions/selfServiceFlowType" }, @@ -4912,10 +5003,6 @@ } } }, - "recoveryFlowState": { - "description": "The state represents the state of the recovery flow.\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed.", - "title": "Recovery Flow State" - }, "recoveryIdentityAddress": { "type": "object", "required": [ @@ -4978,7 +5065,7 @@ ], "properties": { "active": { - "description": "Active, if set, contains the registration method that is being used. It is initially\nnot set.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "Active, if set, contains the registration method that is being used. It is initially\nnot set.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "type": "string", "enum": [ "password", @@ -4987,10 +5074,12 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], - "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" }, "expires_at": { "description": "ExpiresAt is the time (UTC) when the flow expires. If the user still wishes to log in,\na new flow has to be initiated.", @@ -5044,10 +5133,6 @@ } } }, - "registrationFlowState": { - "description": "choose_method: ask the user to choose a method (e.g. registration with email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the registration challenge was passed.", - "title": "State represents the state of this request:" - }, "selfServiceFlowExpiredError": { "description": "Is sent when a flow is expired", "type": "object", @@ -5142,7 +5227,7 @@ "format": "date-time" }, "method": { - "description": "The method used in this authenticator.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", + "description": "The method used in this authenticator.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode", "type": "string", "enum": [ "password", @@ -5151,10 +5236,12 @@ "lookup_secret", "webauthn", "code", + "passkey", + "profile", "link_recovery", "code_recovery" ], - "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" + "x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode" }, "organization": { "description": "The Organization id used for authentication", @@ -5255,6 +5342,10 @@ "state": { "description": "State represents the state of this flow. It knows two states:\n\nshow_form: No user data has been collected, or it is invalid, and thus the form should be shown.\nsuccess: Indicates that the settings flow has been updated successfully with the provided data.\nDone will stay true when repeatedly checking. If set to true, done will revert back to false only\nwhen a flow with invalid (e.g. \"please use a valid phone number\") data was sent." }, + "transient_payload": { + "description": "TransientPayload is used to pass data from the settings flow to hooks and email templates", + "type": "object" + }, "type": { "$ref": "#/definitions/selfServiceFlowType" }, @@ -5263,10 +5354,6 @@ } } }, - "settingsFlowState": { - "description": "show_form: No user data has been collected, or it is invalid, and thus the form should be shown.\nsuccess: Indicates that the settings flow has been updated successfully with the provided data.\nDone will stay true when repeatedly checking. If set to true, done will revert back to false only\nwhen a flow with invalid (e.g. \"please use a valid phone number\") data was sent.", - "title": "State represents the state of this flow. It knows two states:" - }, "successfulAdminIdentitySession": { "type": "object", "required": [ @@ -5309,6 +5396,13 @@ "session" ], "properties": { + "continue_with": { + "description": "Contains a list of actions, that could follow this flow\n\nIt can, for example, this will contain a reference to the verification flow, created as part of the user's\nregistration or the token of the session.", + "type": "array", + "items": { + "$ref": "#/definitions/continueWith" + } + }, "session": { "$ref": "#/definitions/session" }, @@ -5417,7 +5511,7 @@ "$ref": "#/definitions/uiNodeAttributes" }, "group": { - "description": "Group specifies which group (e.g. password authenticator) this node belongs to.\ndefault DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup", + "description": "Group specifies which group (e.g. password authenticator) this node belongs to.\ndefault DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup\nidentifier_first IdentifierFirstGroup", "type": "string", "enum": [ "default", @@ -5428,9 +5522,11 @@ "code", "totp", "lookup_secret", - "webauthn" + "webauthn", + "passkey", + "identifier_first" ], - "x-go-enum-desc": "default DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup" + "x-go-enum-desc": "default DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup\nidentifier_first IdentifierFirstGroup" }, "messages": { "$ref": "#/definitions/uiTexts" @@ -5471,8 +5567,16 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"a\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"a\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "type": "string", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "title": { "$ref": "#/definitions/uiText" @@ -5504,8 +5608,16 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"img\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"img\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "type": "string", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "src": { "description": "The image's source URL.\n\nformat: uri", @@ -5549,9 +5661,9 @@ "$ref": "#/definitions/uiText" }, "maxlength": { - "description": "The maxlength attribute for the input.", + "description": "MaxLength may contain the input's maximum length.", "type": "integer", - "format": "uint64" + "format": "int64" }, "minlength": { "description": "The minlength attribute for the input.", @@ -5563,13 +5675,51 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "type": "string", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "onclick": { - "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.", + "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead.", + "type": "string" + }, + "onclickTrigger": { + "description": "OnClickTrigger may contain a WebAuthn trigger which should be executed on click.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration", + "type": "string", + "enum": [ + "oryWebAuthnRegistration", + "oryWebAuthnLogin", + "oryPasskeyLogin", + "oryPasskeyLoginAutocompleteInit", + "oryPasskeyRegistration", + "oryPasskeySettingsRegistration" + ], + "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration" + }, + "onload": { + "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead.", "type": "string" }, + "onloadTrigger": { + "description": "OnLoadTrigger may contain a WebAuthn trigger which should be executed on load.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration", + "type": "string", + "enum": [ + "oryWebAuthnRegistration", + "oryWebAuthnLogin", + "oryPasskeyLogin", + "oryPasskeyLoginAutocompleteInit", + "oryPasskeyRegistration", + "oryPasskeySettingsRegistration" + ], + "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration" + }, "pattern": { "description": "The input's pattern.", "type": "string" @@ -5644,8 +5794,16 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"script\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"script\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "type": "string", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "nonce": { "description": "Nonce for CSP\n\nA nonce you may want to use to improve your Content Security Policy.\nYou do not have to use this value but if you want to improve your CSP\npolicies you may use it. You can also choose to use your own nonce value!", @@ -5679,8 +5837,16 @@ "type": "string" }, "node_type": { - "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"text\".", - "type": "string" + "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"text\".\ntext Text\ninput Input\nimg Image\na Anchor\nscript Script", + "type": "string", + "enum": [ + "text", + "input", + "img", + "a", + "script" + ], + "x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script" }, "text": { "$ref": "#/definitions/uiText" @@ -5783,6 +5949,10 @@ "csrf_token" ], "properties": { + "address": { + "description": "Address is the address to send the code to, in case that there are multiple addresses. This field\nis only used in two-factor flows and is ineffective for passwordless flows.", + "type": "string" + }, "code": { "description": "Code is the 6 digits code sent to the user", "type": "string" @@ -5802,6 +5972,36 @@ "resend": { "description": "Resend is set when the user wants to resend the code", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + } + } + }, + "updateLoginFlowWithIdentifierFirstMethod": { + "description": "Update Login Flow with Multi-Step Method", + "type": "object", + "required": [ + "method", + "identifier" + ], + "properties": { + "csrf_token": { + "description": "Sending the anti-csrf token is only required for browser login flows.", + "type": "string" + }, + "identifier": { + "description": "Identifier is the email or username of the user trying to log in.", + "type": "string" + }, + "method": { + "description": "Method should be set to \"password\" when logging in using the identifier and password strategy.", + "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -5840,7 +6040,7 @@ "type": "string" }, "id_token": { - "description": "IDToken is an optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", + "description": "IDToken is an optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple\nGoogle", "type": "string" }, "id_token_nonce": { @@ -5859,12 +6059,37 @@ "description": "The identity traits. This is a placeholder for the registration flow.", "type": "object" }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + }, "upstream_parameters": { "description": "UpstreamParameters are the parameters that are passed to the upstream identity provider.\n\nThese parameters are optional and depend on what the upstream identity provider supports.\nSupported parameters are:\n`login_hint` (string): The `login_hint` parameter suppresses the account chooser and either pre-fills the email box on the sign-in form, or selects the proper session.\n`hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`.\n`prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`.", "type": "object" } } }, + "updateLoginFlowWithPasskeyMethod": { + "description": "Update Login Flow with Passkey Method", + "type": "object", + "required": [ + "method" + ], + "properties": { + "csrf_token": { + "description": "Sending the anti-csrf token is only required for browser login flows.", + "type": "string" + }, + "method": { + "description": "Method should be set to \"passkey\" when logging in using the Passkey strategy.", + "type": "string" + }, + "passkey_login": { + "description": "Login a WebAuthn Security Key\n\nThis must contain the ID of the WebAuthN connection.", + "type": "string" + } + } + }, "updateLoginFlowWithPasswordMethod": { "description": "Update Login Flow with Password Method", "type": "object", @@ -5893,6 +6118,10 @@ "password_identifier": { "description": "Identifier is the email or username of the user trying to log in.\nThis field is deprecated!", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -5915,6 +6144,10 @@ "totp_code": { "description": "The TOTP code.", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -5938,6 +6171,10 @@ "description": "Method should be set to \"webAuthn\" when logging in using the WebAuthn strategy.", "type": "string" }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + }, "webauthn_login": { "description": "Login a WebAuthn Security Key\n\nThis must contain the ID of the WebAuthN connection.", "type": "string" @@ -5975,6 +6212,10 @@ "code" ], "x-go-enum-desc": "link RecoveryStrategyLink\ncode RecoveryStrategyCode" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -6002,6 +6243,10 @@ "code" ], "x-go-enum-desc": "link RecoveryStrategyLink\ncode RecoveryStrategyCode" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -6056,7 +6301,7 @@ "type": "string" }, "id_token": { - "description": "IDToken is an optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", + "description": "IDToken is an optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple\nGoogle", "type": "string" }, "id_token_nonce": { @@ -6085,6 +6330,36 @@ } } }, + "updateRegistrationFlowWithPasskeyMethod": { + "description": "Update Registration Flow with Passkey Method", + "type": "object", + "required": [ + "traits", + "method" + ], + "properties": { + "csrf_token": { + "description": "CSRFToken is the anti-CSRF token", + "type": "string" + }, + "method": { + "description": "Method\n\nShould be set to \"passkey\" when trying to add, update, or remove a Passkey.", + "type": "string" + }, + "passkey_register": { + "description": "Register a WebAuthn Security Key\n\nIt is expected that the JSON returned by the WebAuthn registration process\nis included here.", + "type": "string" + }, + "traits": { + "description": "The identity's traits", + "type": "object" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + } + } + }, "updateRegistrationFlowWithPasswordMethod": { "description": "Update Registration Flow with Password Method", "type": "object", @@ -6116,6 +6391,41 @@ } } }, + "updateRegistrationFlowWithProfileMethod": { + "description": "Update Registration Flow with Profile Method", + "type": "object", + "required": [ + "traits", + "method" + ], + "properties": { + "csrf_token": { + "description": "The Anti-CSRF Token\n\nThis token is only required when performing browser flows.", + "type": "string" + }, + "method": { + "description": "Method\n\nShould be set to profile when trying to update a profile.", + "type": "string" + }, + "screen": { + "description": "Screen requests navigation to a previous screen.\n\nThis must be set to credential-selection to go back to the credential\nselection screen.\ncredential-selection RegistrationScreenCredentialSelection nolint:gosec // not a credential\nprevious RegistrationScreenPrevious", + "type": "string", + "enum": [ + "credential-selection", + "previous" + ], + "x-go-enum-desc": "credential-selection RegistrationScreenCredentialSelection nolint:gosec // not a credential\nprevious RegistrationScreenPrevious" + }, + "traits": { + "description": "Traits\n\nThe identity's traits.", + "type": "object" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + } + } + }, "updateRegistrationFlowWithWebAuthnMethod": { "description": "Update Registration Flow with WebAuthn Method", "type": "object", @@ -6184,6 +6494,10 @@ "method": { "description": "Method\n\nShould be set to \"lookup\" when trying to add, update, or remove a lookup pairing.", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -6210,6 +6524,10 @@ "description": "The identity's traits\n\nin: body", "type": "object" }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + }, "unlink": { "description": "Unlink this provider\n\nEither this or `link` must be set.\n\ntype: string\nin: body", "type": "string" @@ -6220,6 +6538,31 @@ } } }, + "updateSettingsFlowWithPasskeyMethod": { + "description": "Update Settings Flow with Passkey Method", + "type": "object", + "required": [ + "method" + ], + "properties": { + "csrf_token": { + "description": "CSRFToken is the anti-CSRF token", + "type": "string" + }, + "method": { + "description": "Method\n\nShould be set to \"passkey\" when trying to add, update, or remove a webAuthn pairing.", + "type": "string" + }, + "passkey_remove": { + "description": "Remove a WebAuthn Security Key\n\nThis must contain the ID of the WebAuthN connection.", + "type": "string" + }, + "passkey_settings_register": { + "description": "Register a WebAuthn Security Key\n\nIt is expected that the JSON returned by the WebAuthn registration process\nis included here.", + "type": "string" + } + } + }, "updateSettingsFlowWithPasswordMethod": { "description": "Update Settings Flow with Password Method", "type": "object", @@ -6239,6 +6582,10 @@ "password": { "description": "Password is the updated password", "type": "string" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -6261,6 +6608,10 @@ "traits": { "description": "Traits\n\nThe identity's traits.", "type": "object" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -6286,6 +6637,10 @@ "totp_unlink": { "description": "UnlinkTOTP if true will remove the TOTP pairing,\neffectively removing the credential. This can be used\nto set up a new TOTP device.", "type": "boolean" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -6304,6 +6659,10 @@ "description": "Method\n\nShould be set to \"webauthn\" when trying to add, update, or remove a webAuthn pairing.", "type": "string" }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" + }, "webauthn_register": { "description": "Register a WebAuthn Security Key\n\nIt is expected that the JSON returned by the WebAuthn registration process\nis included here.", "type": "string" @@ -6348,6 +6707,10 @@ "code" ], "x-go-enum-desc": "link VerificationStrategyLink\ncode VerificationStrategyCode" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -6375,6 +6738,10 @@ "code" ], "x-go-enum-desc": "link VerificationStrategyLink\ncode VerificationStrategyCode" + }, + "transient_payload": { + "description": "Transient data to pass along to any webhooks", + "type": "object" } } }, @@ -6472,6 +6839,10 @@ "state": { "description": "State represents the state of this request:\n\nchoose_method: ask the user to choose a method (e.g. verify your email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the verification challenge was passed." }, + "transient_payload": { + "description": "TransientPayload is used to pass data from the verification flow to hooks and email templates", + "type": "object" + }, "type": { "$ref": "#/definitions/selfServiceFlowType" }, @@ -6480,10 +6851,6 @@ } } }, - "verificationFlowState": { - "description": "The state represents the state of the verification flow.\n\nchoose_method: ask the user to choose a method (e.g. recover account via email)\nsent_email: the email has been sent to the user\npassed_challenge: the request was successful and the recovery challenge was passed.", - "title": "Verification Flow State" - }, "version": { "type": "object", "properties": { @@ -6499,7 +6866,7 @@ }, "responses": { "emptyResponse": { - "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 201." + "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 204." }, "identitySchemas": { "description": "List Identity JSON Schemas Response", @@ -6628,4 +6995,4 @@ }, "x-forwarded-proto": "string", "x-request-id": "string" -} \ No newline at end of file +} diff --git a/test/e2e/cypress.config.ts b/test/e2e/cypress.config.ts index 45ddc9bbd0c3..828b10290695 100644 --- a/test/e2e/cypress.config.ts +++ b/test/e2e/cypress.config.ts @@ -22,6 +22,7 @@ export default defineConfig({ runMode: 6, openMode: 1, }, + experimentalRunAllSpecs: true, videosFolder: "cypress/videos", screenshotsFolder: "cypress/screenshots", excludeSpecPattern: ["**/*snapshots.js", "playwright/**"], diff --git a/test/e2e/cypress/helpers/express.ts b/test/e2e/cypress/helpers/express.ts index 138cf433fc5b..4631edc34426 100644 --- a/test/e2e/cypress/helpers/express.ts +++ b/test/e2e/cypress/helpers/express.ts @@ -1,7 +1,7 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import { APP_URL, SPA_URL } from "./index" +import { APP_URL } from "./index" export const routes = { base: APP_URL, diff --git a/test/e2e/cypress/helpers/index.ts b/test/e2e/cypress/helpers/index.ts index 1fae8d176adb..52bcb339ad50 100644 --- a/test/e2e/cypress/helpers/index.ts +++ b/test/e2e/cypress/helpers/index.ts @@ -37,7 +37,7 @@ export const assertRecoveryAddress = expect(address.value).to.equal(email) } -export const parseHtml = (html) => +export const parseHtml = (html: string) => new DOMParser().parseFromString(html, "text/html") export const APP_URL = ( diff --git a/test/e2e/cypress/helpers/webhook.ts b/test/e2e/cypress/helpers/webhook.ts index fdc37b3fbc68..a5da27abae61 100644 --- a/test/e2e/cypress/helpers/webhook.ts +++ b/test/e2e/cypress/helpers/webhook.ts @@ -8,7 +8,7 @@ const WEBHOOK_TARGET = "https://webhook-target-gsmwn5ab4a-uc.a.run.app" const documentUrl = (key: string) => `${WEBHOOK_TARGET}/documents/${key}` const jsonnet = Buffer.from("function(ctx) ctx").toString("base64") -export const testRegistrationWebhook = ( +export const testFlowWebhook = ( configSetup: ( hooks: Array<{ hook: string; config?: any }>, ) => Cypress.Chainable, @@ -24,7 +24,6 @@ export const testRegistrationWebhook = ( method: "PUT", }, }, - { hook: "session" }, ]) const transient_payload = { @@ -34,27 +33,31 @@ export const testRegistrationWebhook = ( }, consent: true, } - cy.intercept("POST", /.*\/self-service\/registration.*/, (req) => { - switch (typeof req.body) { - case "string": - req.body = - req.body + - "&transient_payload=" + - encodeURIComponent(JSON.stringify(transient_payload)) - break - case "object": - req.body = { - ...req.body, - transient_payload, - } - break + cy.intercept( + "POST", + /.*\/self-service\/(registration|login|recovery|verification|settings).*/, + (req) => { + switch (typeof req.body) { + case "string": + req.body = + req.body + + "&transient_payload=" + + encodeURIComponent(JSON.stringify(transient_payload)) + break + case "object": + req.body = { + ...req.body, + transient_payload, + } + break - default: - fail() - break - } - req.continue() - }) + default: + fail() + break + } + req.continue() + }, + ) act() diff --git a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts index 477a149f9b26..244d40ab8132 100644 --- a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts @@ -83,7 +83,7 @@ context("Login error messages with code method", () => { "An email containing a code has been sent to the email address you provided", ) - cy.get(Selectors[app]["code"]).type("invalid-code") + cy.get(Selectors[app]["code"]).type("123456") cy.submitCodeForm(app) cy.get('[data-testid="ui/message/4010008"]').should( @@ -113,7 +113,7 @@ context("Login error messages with code method", () => { .type(gen.email(), { force: true }) } - cy.get(Selectors[app]["code"]).type("invalid-code") + cy.get(Selectors[app]["code"]).type("123456") cy.submitCodeForm(app) if (app !== "express") { @@ -147,7 +147,7 @@ context("Login error messages with code method", () => { ) } - cy.get(Selectors[app]["code"]).type("invalid-code") + cy.get(Selectors[app]["code"]).type("123456") cy.removeAttribute([Selectors[app]["identity"]], "required") cy.get(Selectors[app]["identity"]).type("{selectall}{backspace}", { @@ -160,6 +160,12 @@ context("Login error messages with code method", () => { "contain", "Property identifier is missing", ) + } else if (app === "react") { + // The backspace trick is not working in React. + cy.get('[data-testid="ui/message/4010008"]').should( + "contain", + "code is invalid", + ) } else { cy.get('[data-testid="ui/message/4000002"]').should( "contain", diff --git a/test/e2e/cypress/integration/profiles/code/login/success.spec.ts b/test/e2e/cypress/integration/profiles/code/login/success.spec.ts index 958f367612ab..94ce753f0322 100644 --- a/test/e2e/cypress/integration/profiles/code/login/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/login/success.spec.ts @@ -138,6 +138,12 @@ context("Login success with code method", () => { cy.get(Selectors[app]["submit"]).click() }) + if (app === "express") { + cy.url().should("match", /\/welcome/) + } else { + cy.get('[data-testid="session-content"]').should("contain", email) + } + if (app === "mobile") { cy.get('[data-testid="session-token"]').then((token) => { cy.getSession({ @@ -206,6 +212,12 @@ context("Login success with code method", () => { cy.get(Selectors[app]["submit"]).click() + if (app === "express") { + cy.url().should("match", /\/welcome/) + } else { + cy.get('[data-testid="session-content"]').should("contain", email) + } + if (app === "express") { cy.get('a[href*="sessions"').click() } @@ -274,6 +286,12 @@ context("Login success with code method", () => { cy.get(Selectors[app]["submit"]).click() }) + if (app === "express") { + cy.url().should("match", /\/welcome/) + } else { + cy.get('[data-testid="session-content"]').should("contain", email) + } + if (app === "mobile") { cy.get('[data-testid="session-token"]').then((token) => { cy.getSession({ diff --git a/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts index ef435991f736..4570c1c5cc60 100644 --- a/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts @@ -74,7 +74,7 @@ context("Registration error messages with code method", () => { "An email containing a code has been sent to the email address you provided", ) - cy.get(Selectors[app]["code"]).type("invalid-code") + cy.get(Selectors[app]["code"]).type("123456") cy.submitCodeForm(app) cy.get('[data-testid="ui/message/4040003"]').should( @@ -111,7 +111,7 @@ context("Registration error messages with code method", () => { .type("changed-email@email.com", { force: true }) } - cy.get(Selectors[app]["code"]).type("invalid-code") + cy.get(Selectors[app]["code"]).type("123456") cy.submitCodeForm(app) if (app !== "express") { @@ -174,7 +174,7 @@ context("Registration error messages with code method", () => { }) cy.removeAttribute([Selectors[app]["email"]], "required") } - cy.get(Selectors[app]["code"]).type("invalid-code") + cy.get(Selectors[app]["code"]).type("123456") cy.submitCodeForm(app) diff --git a/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts index 53fd71ac0f66..c715d80cd86e 100644 --- a/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/code/registration/success.spec.ts @@ -6,6 +6,53 @@ import { gen, MOBILE_URL } from "../../../../helpers" import { routes as express } from "../../../../helpers/express" import { routes as react } from "../../../../helpers/react" +const Selectors = { + mobile: { + identifier: "[data-testid='field/identifier']", + recoveryEmail: "[data-testid='field/email']", + email: "[data-testid='traits.email']", + email2: "[data-testid='traits.email2']", + tos: "[data-testid='traits.tos']", + username: "[data-testid='traits.username']", + code: "[data-testid='field/code'] input", + recoveryCode: "[data-testid='code']", + submitCode: "[data-testid='field/method/code']", + resendCode: "[data-testid='field/resend/code']", + submitRecovery: "[data-testid='field/method/code']", + codeHiddenMethod: "[data-testid='field/method/code']", + }, + express: { + identifier: "[data-testid='login-flow-code'] input[name='identifier']", + recoveryEmail: "input[name=email]", + email: "[data-testid='registration-flow-code'] input[name='traits.email']", + email2: + "[data-testid='registration-flow-code'] input[name='traits.email2']", + tos: "[data-testid='registration-flow-code'] [name='traits.tos'] + label", + username: + "[data-testid='registration-flow-code'] input[name='traits.username']", + code: "input[name='code']", + recoveryCode: "input[name=code]", + submitRecovery: "button[name=method][value=code]", + submitCode: "button[name='method'][value='code']", + resendCode: "button[name='resend'][value='code']", + codeHiddenMethod: "input[name='method'][value='code'][type='hidden']", + }, + react: { + identifier: "input[name='identifier']", + recoveryEmail: "input[name=email]", + email: "input[name='traits.email']", + email2: "input[name='traits.email2']", + tos: "[name='traits.tos'] + label", + username: "input[name='traits.username']", + code: "input[name='code']", + recoveryCode: "input[name=code]", + submitRecovery: "button[name=method][value=code]", + submitCode: "button[name='method'][value='code']", + resendCode: "button[name='resend'][value='code']", + codeHiddenMethod: "input[name='method'][value='code'][type='hidden']", + }, +} + context("Registration success with code method", () => { ;[ { @@ -31,57 +78,7 @@ context("Registration success with code method", () => { }, ].forEach(({ route, login, recovery, profile, app }) => { describe(`for app ${app}`, () => { - const Selectors = { - mobile: { - identifier: "[data-testid='field/identifier']", - recoveryEmail: "[data-testid='field/email']", - email: "[data-testid='traits.email']", - email2: "[data-testid='traits.email2']", - tos: "[data-testid='traits.tos']", - username: "[data-testid='traits.username']", - code: "[data-testid='field/code']", - recoveryCode: "[data-testid='code']", - submitCode: "[data-testid='field/method/code']", - resendCode: "[data-testid='field/method/resend']", - submitRecovery: "[data-testid='field/method/code']", - codeHiddenMethod: "[data-testid='field/method/code']", - }, - express: { - identifier: - "[data-testid='login-flow-code'] input[name='identifier']", - recoveryEmail: "input[name=email]", - email: - "[data-testid='registration-flow-code'] input[name='traits.email']", - email2: - "[data-testid='registration-flow-code'] input[name='traits.email2']", - tos: "[data-testid='registration-flow-code'] [name='traits.tos'] + label", - username: - "[data-testid='registration-flow-code'] input[name='traits.username']", - code: "input[name='code']", - recoveryCode: "input[name=code]", - submitRecovery: "button[name=method][value=code]", - submitCode: "button[name='method'][value='code']", - resendCode: "button[name='resend'][value='code']", - codeHiddenMethod: "input[name='method'][value='code'][type='hidden']", - }, - react: { - identifier: "input[name='identifier']", - recoveryEmail: "input[name=email]", - email: "input[name='traits.email']", - email2: "input[name='traits.email2']", - tos: "[name='traits.tos'] + label", - username: "input[name='traits.username']", - code: "input[name='code']", - recoveryCode: "input[name=code]", - submitRecovery: "button[name=method][value=code]", - submitCode: "button[name='method'][value='code']", - resendCode: "button[name='resend'][value='code']", - codeHiddenMethod: "input[name='method'][value='code'][type='hidden']", - }, - } - before(() => { - cy.deleteMail() cy.useConfigProfile(profile) if (app !== "mobile") { cy.proxy(app) @@ -94,11 +91,11 @@ context("Registration success with code method", () => { cy.visit(route) }) - it("should be able to resend the registration code", async () => { + it("should be able to resend the registration code", () => { const email = gen.email() cy.get(Selectors[app]["email"]).type(email) - cy.get(`${Selectors[app]["tos"]} + label`).click() + cy.get(Selectors[app]["tos"]).click() cy.submitCodeForm(app) cy.get('[data-testid="ui/message/1040005"]').should( @@ -110,7 +107,6 @@ context("Registration success with code method", () => { cy.wrap(code).as("code1"), ) - cy.get(Selectors[app]["email"]).should("have.value", email) cy.get(Selectors[app]["codeHiddenMethod"]).should("exist") cy.get(Selectors[app]["resendCode"]).click() @@ -180,7 +176,13 @@ context("Registration success with code method", () => { cy.get(Selectors[app]["submitCode"]).click() }) + if (app === "express") { + cy.url().should("match", /\/welcome/) + } else { + cy.get('[data-testid="session-content"]').should("contain", email) + } if (app === "mobile") { + cy.get('[data-testid="session-token"]').should("not.be.empty") cy.get('[data-testid="session-token"]').then((token) => { cy.getSession({ expectAal: "aal1", @@ -190,9 +192,6 @@ context("Registration success with code method", () => { cy.wrap(session).as("session") }) }) - - cy.get('[data-testid="session-content"]').should("contain", email) - cy.get('[data-testid="session-token"]').should("not.be.empty") } else { cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then( (session) => { @@ -236,7 +235,14 @@ context("Registration success with code method", () => { cy.get(Selectors[app]["submitCode"]).click() }) + if (app === "express") { + cy.url().should("match", /\/welcome/) + } else { + cy.get('[data-testid="session-content"]').should("contain", email) + } + if (app === "mobile") { + cy.get('[data-testid="session-token"]').should("not.be.empty") cy.get('[data-testid="session-token"]').then((token) => { cy.getSession({ expectAal: "aal1", @@ -246,9 +252,6 @@ context("Registration success with code method", () => { cy.wrap(session).as("session") }) }) - - cy.get('[data-testid="session-content"]').should("contain", email) - cy.get('[data-testid="session-token"]').should("not.be.empty") } else { cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then( (session) => { @@ -305,6 +308,9 @@ context("Registration success with code method", () => { { hook: "session", }, + { + hook: "show_verification_ui", + }, ]) // Setup complex schema @@ -335,6 +341,7 @@ context("Registration success with code method", () => { cy.get(Selectors[app]["submitCode"]).click() }, ) + cy.get('[data-testid="ui/message/1080003"]').should("be.visible") if (app === "mobile") { cy.visit(MOBILE_URL + "/Home") @@ -359,8 +366,14 @@ context("Registration success with code method", () => { cy.get(Selectors[app]["code"]).type(code) cy.get(Selectors[app]["submitCode"]).click() }) + if (app === "express") { + cy.url().should("match", /\/welcome/) + } else { + cy.get('[data-testid="session-content"]').should("contain", email) + } if (app === "mobile") { + cy.get('[data-testid="session-token"]').should("not.be.empty") cy.get('[data-testid="session-token"]').then((token) => { cy.getSession({ expectAal: "aal1", @@ -370,9 +383,6 @@ context("Registration success with code method", () => { cy.wrap(session).as("session") }) }) - - cy.get('[data-testid="session-content"]').should("contain", email) - cy.get('[data-testid="session-token"]').should("not.be.empty") } else { cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then( (session) => { diff --git a/test/e2e/cypress/integration/profiles/email/logout/success.spec.ts b/test/e2e/cypress/integration/profiles/email/logout/success.spec.ts index f13358166afb..3926f21012b1 100644 --- a/test/e2e/cypress/integration/profiles/email/logout/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/email/logout/success.spec.ts @@ -75,12 +75,12 @@ context("Testing logout flows", () => { cy.visit(settings, { qs: { - return_to: "https://www.ory.sh", + return_to: "https://www.example.org", }, }) cy.get("a[href*='logout']").click() - cy.location("host").should("eq", "www.ory.sh") + cy.location("host").should("eq", "www.example.org") }) it("should be able to sign out on welcome page", () => { @@ -94,12 +94,12 @@ context("Testing logout flows", () => { cy.visit(welcome, { qs: { - return_to: "https://www.ory.sh", + return_to: "https://www.example.org", }, }) cy.get("a[href*='logout']").click() - cy.location("host").should("eq", "www.ory.sh") + cy.location("host").should("eq", "www.example.org") }) it("should be able to sign out at 2fa page", () => { @@ -122,7 +122,7 @@ context("Testing logout flows", () => { cy.logout() cy.visit(route, { qs: { - return_to: "https://www.ory.sh", + return_to: "https://www.example.org", }, }) @@ -135,7 +135,7 @@ context("Testing logout flows", () => { cy.get("a[href*='logout']").click() - cy.location("host").should("eq", "www.ory.sh") + cy.location("host").should("eq", "www.example.org") cy.useLookupSecrets(false) }) }) diff --git a/test/e2e/cypress/integration/profiles/mfa/code.spec.ts b/test/e2e/cypress/integration/profiles/mfa/code.spec.ts index 7961aa850de9..313e85e5382c 100644 --- a/test/e2e/cypress/integration/profiles/mfa/code.spec.ts +++ b/test/e2e/cypress/integration/profiles/mfa/code.spec.ts @@ -3,7 +3,6 @@ import { gen, website } from "../../../helpers" import { routes as express } from "../../../helpers/express" -import { routes as react } from "../../../helpers/react" context("2FA code", () => { ;[ @@ -31,72 +30,110 @@ context("2FA code", () => { let email: string let password: string - beforeEach(() => { - email = gen.email() - password = gen.password() - cy.useConfig((builder) => - builder - .longPrivilegedSessionTime() - .useLaxAal() - .enableCode() - .enableCodeMFA(), - ) - - cy.register({ - email, - password, - fields: { "traits.website": website }, + describe("when using highest_available aal", () => { + beforeEach(() => { + cy.useConfig((builder) => + builder + .longPrivilegedSessionTime() + .useHighestAvailable() + .enableCodeMFA(), + ) }) - cy.deleteMail() - cy.visit(login + "?aal=aal2&via=email") - }) - it("should be asked to sign in with 2fa if set up", () => { - cy.get("input[name='identifier']").type(email) - cy.contains("Continue with code").click() + it("should show second factor screen on whoami call", () => { + email = gen.email() + password = gen.password() + cy.register({ + email, + password, + fields: { "traits.website": website }, + }) + cy.deleteMail() - cy.get("input[name='code']").should("be.visible") - cy.getLoginCodeFromEmail(email).then((code) => { - cy.get("input[name='code']").type(code) - cy.contains("Submit").click() - }) + cy.visit(settings) + cy.location("pathname").should("contain", "/login") // we get redirected to login + + cy.get("[type='submit'][name='address']").should("be.visible").click() + + cy.getLoginCodeFromEmail(email).then((code) => { + cy.get("input[name='code']").type(code) + cy.contains("Continue").click() + }) - cy.getSession({ - expectAal: "aal2", - expectMethods: ["password", "code"], + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "code"], + }) }) }) - it("can't use different email in 2fa request", () => { - cy.get("input[name='identifier']").type(gen.email()) - cy.contains("Continue with code").click() + describe("when using aal1 required aal", () => { + beforeEach(() => { + email = gen.email() + password = gen.password() + cy.useConfig((builder) => + builder + .longPrivilegedSessionTime() + .useLaxAal() + .enableCode() + .enableCodeMFA(), + ) + + cy.register({ + email, + password, + fields: { "traits.website": website }, + }) + cy.deleteMail() + cy.visit(login + "?aal=aal2&via=email") + }) + + it("should be asked to sign in with 2fa if set up", () => { + cy.get("*[name='address']").click() - cy.get("*[data-testid='ui/message/4010010']").should("be.visible") - cy.get("input[name='code']").should("not.exist") - cy.get("input[name='identifier']").should("be.visible") + cy.get("input[name='code']").should("be.visible") + cy.getLoginCodeFromEmail(email).then((code) => { + cy.get("input[name='code']").type(code) + cy.contains("Continue").click() + }) - // The current session should be unchanged - cy.getSession({ - expectAal: "aal1", - expectMethods: ["password"], + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "code"], + }) }) - }) - it("entering wrong code should not invalidate correct codes", () => { - cy.get("input[name='identifier']").type(email) - cy.contains("Continue with code").click() + it("can't use different email in 2fa request", () => { + cy.get('[name="address"]').invoke("attr", "value", gen.email()) + + cy.get('[name="address"]').click() + + cy.get("*[data-testid='ui/message/4000035']").should("be.visible") + cy.get("input[name='code']").should("not.exist") + cy.get("[name='address']").should("be.visible") - cy.get("input[name='code']").should("be.visible") - cy.get("input[name='code']").type("123456") - cy.contains("Submit").click() - cy.getLoginCodeFromEmail(email).then((code) => { - cy.get("input[name='code']").type(code) - cy.contains("Submit").click() + // The current session should be unchanged + cy.getSession({ + expectAal: "aal1", + expectMethods: ["password"], + }) }) - cy.getSession({ - expectAal: "aal2", - expectMethods: ["password", "code"], + it("entering wrong code should not invalidate correct codes", () => { + cy.get("*[name='address']").click() + + cy.get("input[name='code']").should("be.visible").type("123456") + + cy.contains("Continue").click() + cy.getLoginCodeFromEmail(email).then((code) => { + cy.get("input[name='code']").type(code) + cy.contains("Continue").click() + }) + + cy.getSession({ + expectAal: "aal2", + expectMethods: ["password", "code"], + }) }) }) }) diff --git a/test/e2e/cypress/integration/profiles/mfa/lookup.spec.ts b/test/e2e/cypress/integration/profiles/mfa/lookup.spec.ts index 40e6c7e7984a..1bb4e2f94898 100644 --- a/test/e2e/cypress/integration/profiles/mfa/lookup.spec.ts +++ b/test/e2e/cypress/integration/profiles/mfa/lookup.spec.ts @@ -34,6 +34,7 @@ context("2FA lookup secrets", () => { beforeEach(() => { cy.visit(base) cy.clearAllCookies() + cy.useConfig((builder) => builder.disableCodeMfa()) email = gen.email() password = gen.password() cy.registerApi({ @@ -191,10 +192,9 @@ context("2FA lookup secrets", () => { cy.visit(settings) cy.get('button[name="lookup_secret_reveal"]').click() cy.getLookupSecrets().should((c) => { - let newCodes = codes - newCodes[0] = "Used" - newCodes[1] = "Used" - expect(c).to.eql(newCodes) + expect(c.slice(2)).to.eql(codes.slice(2)) + expect(c[0]).to.match(/(Secret was used at )|(Used)/g) + expect(c[1]).to.match(/(Secret was used at )|(Used)/g) }) // Regenerating the codes means the old one become invalid @@ -234,9 +234,8 @@ context("2FA lookup secrets", () => { cy.visit(settings) cy.get('button[name="lookup_secret_reveal"]').click() cy.getLookupSecrets().should((c) => { - let newCodes = regenCodes - newCodes[0] = "Used" - expect(c).to.eql(newCodes) + expect(c.slice(1)).to.eql(regenCodes.slice(1)) + expect(c[0]).to.match(/(Secret was used at )|(Used)/g) }) }) diff --git a/test/e2e/cypress/integration/profiles/mfa/settings.spec.ts b/test/e2e/cypress/integration/profiles/mfa/settings.spec.ts index 2b2e312a136a..329cb0ffc04b 100644 --- a/test/e2e/cypress/integration/profiles/mfa/settings.spec.ts +++ b/test/e2e/cypress/integration/profiles/mfa/settings.spec.ts @@ -39,6 +39,7 @@ context("2FA UI settings tests", () => { beforeEach(() => { cy.clearAllCookies() + cy.useConfig((builder) => builder.disableCodeMfa()) cy.login({ email, password, cookieUrl: base }) cy.visit(settings) }) diff --git a/test/e2e/cypress/integration/profiles/mfa/totp.spec.ts b/test/e2e/cypress/integration/profiles/mfa/totp.spec.ts index a193bcb6ceed..d3523dc241b1 100644 --- a/test/e2e/cypress/integration/profiles/mfa/totp.spec.ts +++ b/test/e2e/cypress/integration/profiles/mfa/totp.spec.ts @@ -34,7 +34,7 @@ context("2FA TOTP", () => { beforeEach(() => { cy.useConfig((builder) => - builder.longPrivilegedSessionTime().useLaxAal(), + builder.longPrivilegedSessionTime().useLaxAal().disableCodeMfa(), ) email = gen.email() password = gen.password() diff --git a/test/e2e/cypress/integration/profiles/mfa/webauthn.spec.ts b/test/e2e/cypress/integration/profiles/mfa/webauthn.spec.ts index 07be1001d323..9e6b260a7922 100644 --- a/test/e2e/cypress/integration/profiles/mfa/webauthn.spec.ts +++ b/test/e2e/cypress/integration/profiles/mfa/webauthn.spec.ts @@ -37,6 +37,7 @@ context("2FA WebAuthn", () => { beforeEach(() => { cy.clearAllCookies() + cy.useConfig((builder) => builder.disableCodeMfa()) email = gen.email() password = gen.password() cy.registerApi({ diff --git a/test/e2e/cypress/integration/profiles/mobile/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/mobile/registration/success.spec.ts index 100bc50ab9c4..2a2d6a1c24d5 100644 --- a/test/e2e/cypress/integration/profiles/mobile/registration/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/mobile/registration/success.spec.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { gen, MOBILE_URL, website } from "../../../../helpers" -import { testRegistrationWebhook } from "../../../../helpers/webhook" +import { testFlowWebhook } from "../../../../helpers/webhook" context("Mobile Profile", () => { describe("Login Flow Success", () => { @@ -29,8 +29,12 @@ context("Mobile Profile", () => { }) it("should pass transient_payload to webhook", () => { - testRegistrationWebhook( - (hooks) => cy.setupHooks("registration", "after", "password", hooks), + testFlowWebhook( + (hooks) => + cy.setupHooks("registration", "after", "password", [ + ...hooks, + { hook: "session" }, + ]), () => { const email = gen.email() const password = gen.password() diff --git a/test/e2e/cypress/integration/profiles/mobile/settings/success.spec.ts b/test/e2e/cypress/integration/profiles/mobile/settings/success.spec.ts index e86fc16b8100..a6ef326994ba 100644 --- a/test/e2e/cypress/integration/profiles/mobile/settings/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/mobile/settings/success.spec.ts @@ -25,6 +25,7 @@ context("Mobile Profile", () => { beforeEach(() => { cy.loginMobile({ email, password }) + cy.location("pathname").should("not.contain", "/Login") // In our case we enabled setCookies for API request to support mobile webview // Therefore, after login to make test consistent with upstream we need to clear // cookies. @@ -72,6 +73,7 @@ context("Mobile Profile", () => { fields: { "traits.website": website }, }) cy.loginMobile({ email, password }) + cy.location("pathname").should("not.contain", "/Login") cy.visit(MOBILE_URL + "/Settings") }) diff --git a/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts b/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts index 866f4344eda6..853c165df949 100644 --- a/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts @@ -39,7 +39,7 @@ context("Social Sign In Successes", () => { cy.loginOidc({ app, url: login }) }) - it.only("should be able to sign up and link existing account", () => { + it("should be able to sign up and link existing account", () => { const email = gen.email() const password = gen.password() @@ -60,7 +60,6 @@ context("Social Sign In Successes", () => { cy.noSession() // Log in with the same identifier through the login flow. This should link the accounts. - cy.get(`${appPrefix(app)}input[name="identifier"]`).type(email) cy.get('input[name="password"]').type(password) cy.submitPasswordForm() cy.location("pathname").should("not.contain", "/login") @@ -70,7 +69,7 @@ context("Social Sign In Successes", () => { cy.visit(settings) cy.get('[value="hydra"]') .should("have.attr", "name", "unlink") - .should("contain.text", "Unlink hydra") + .should("contain.text", "Unlink Ory") }) it("should be able to sign up with redirects", () => { diff --git a/test/e2e/cypress/integration/profiles/oidc/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/oidc/registration/success.spec.ts index fe32cbbf61a2..132845623f31 100644 --- a/test/e2e/cypress/integration/profiles/oidc/registration/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/oidc/registration/success.spec.ts @@ -4,7 +4,7 @@ import { appPrefix, gen, website } from "../../../../helpers" import { routes as express } from "../../../../helpers/express" import { routes as react } from "../../../../helpers/react" -import { testRegistrationWebhook } from "../../../../helpers/webhook" +import { testFlowWebhook } from "../../../../helpers/webhook" context("Social Sign Up Successes", () => { ;[ @@ -104,8 +104,12 @@ context("Social Sign Up Successes", () => { }) it("should pass transient_payload to webhook", () => { - testRegistrationWebhook( - (hooks) => cy.setupHooks("registration", "after", "oidc", hooks), + testFlowWebhook( + (hooks) => + cy.setupHooks("registration", "after", "oidc", [ + ...hooks, + { hook: "session" }, + ]), () => { const email = gen.email() cy.registerOidc({ @@ -190,9 +194,9 @@ context("Social Sign Up Successes", () => { app, email, website, - route: registration + "?return_to=https://www.ory.sh/", + route: registration + "?return_to=https://www.example.org/", }) - cy.location("href").should("eq", "https://www.ory.sh/") + cy.location("href").should("eq", "https://www.example.org/") cy.logout() }) @@ -248,7 +252,6 @@ context("Social Sign Up Successes", () => { cy.location("href").should("contain", "/login") - cy.get("[name='provider'][value='hydra']").should("be.visible") cy.get("[name='provider'][value='google']").should("be.visible") cy.get("[name='provider'][value='github']").should("be.visible") @@ -256,7 +259,6 @@ context("Social Sign Up Successes", () => { cy.get("[data-testid='forgot-password-link']").should("be.visible") } - cy.get("input[name='identifier']").type(email) cy.get("input[name='password']").type(password) cy.submitPasswordForm() cy.getSession() diff --git a/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts b/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts index 674e24e0d668..8eaec262b303 100644 --- a/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts @@ -44,7 +44,7 @@ context("Social Sign In Settings Success", () => { cy.get('[data-testid="ui/message/1010016"]').should( "contain.text", - "Signing in will link your account", + "as another way to sign in.", ) cy.noSession() @@ -94,7 +94,7 @@ context("Social Sign In Settings Success", () => { cy.get('[value="hydra"]') .should("have.attr", "name", "unlink") - .should("contain.text", "Unlink hydra") + .should("contain.text", "Unlink Ory") }) it("should link google", () => { diff --git a/test/e2e/cypress/integration/profiles/passkey/flows.spec.ts b/test/e2e/cypress/integration/profiles/passkey/flows.spec.ts new file mode 100644 index 000000000000..78426b7cd9f4 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/passkey/flows.spec.ts @@ -0,0 +1,302 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { gen } from "../../../helpers" +import { routes as express } from "../../../helpers/express" +import { routes as react } from "../../../helpers/react" +import { testFlowWebhook } from "../../../helpers/webhook" + +const signup = (registration: string, app: string, email = gen.email()) => { + cy.visit(registration) + + const websiteTrait = `${ + app === "express" ? `form[data-testid="passkey-flow"]` : "" + } input[name="traits.website"]` + const emailTrait = `${ + app === "express" ? `form[data-testid="passkey-flow"]` : "" + } input[name="traits.email"]` + + cy.get(emailTrait).type(email) + cy.get(websiteTrait).type("https://www.ory.sh") + cy.get('[name="passkey_register_trigger"]').click() + + cy.wait(1000) + + cy.getSession({ + expectAal: "aal1", + expectMethods: ["passkey"], + }).then((session) => { + expect(session.identity.traits.email).to.equal(email) + expect(session.identity.traits.website).to.equal("https://www.ory.sh") + }) +} + +context("Passkey registration", () => { + before(() => { + cy.task("resetCRI", {}) + }) + after(() => { + cy.task("resetCRI", {}) + }) + ;[ + { + login: react.login, + registration: express.registration, + settings: react.settings, + base: react.base, + app: "react" as "react", + profile: "passkey", + }, + { + login: express.login, + registration: express.registration, + settings: express.settings, + base: express.base, + app: "express" as "express", + profile: "passkey", + }, + ].forEach(({ registration, login, profile, app, base, settings }) => { + describe(`for app ${app}`, () => { + let authenticator: any + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + + cy.task("sendCRI", { + query: "WebAuthn.enable", + opts: {}, + }) + .then(() => { + cy.task("sendCRI", { + query: "WebAuthn.addVirtualAuthenticator", + opts: { + options: { + protocol: "ctap2", + transport: "internal", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, + }, + }, + }) + }) + .then((result) => { + authenticator = result + cy.log("authenticator ID:", authenticator) + }) + + cy.longPrivilegedSessionTime() + }) + + beforeEach(() => { + cy.clearAllCookies() + cy.task("sendCRI", { + query: "WebAuthn.clearCredentials", + opts: authenticator, + }) + }) + + after(() => { + cy.task("sendCRI", { + query: "WebAuthn.removeVirtualAuthenticator", + opts: authenticator, + }) + }) + + it("should register after validation errors", () => { + cy.visit(registration) + + // the browser will prevent the form from being submitted if the input field is required + // we should remove the required attribute to simulate the data not being sent + cy.removeAttribute( + ['input[name="traits.email"]', 'input[name="traits.website"]'], + "required", + ) + + cy.get(`input[name="traits.website"]`).then(($el) => { + $el.removeAttr("type") + }) + + const websiteTrait = `${ + app === "express" ? `form[data-testid="passkey-flow"]` : "" + } input[name="traits.website"]` + + const emailTrait = `${ + app === "express" ? `form[data-testid="passkey-flow"]` : "" + } input[name="traits.email"]` + + cy.get(websiteTrait).type("b") + cy.get('[name="passkey_register_trigger"]').click() + + cy.get('[data-testid="ui/message/4000002"]').should("to.exist") + cy.get('[data-testid="ui/message/4000001"]').should("to.exist") + cy.get(websiteTrait).should("have.value", "b") + + const email = gen.email() + cy.get(emailTrait).type(email) + cy.get('[name="passkey_register_trigger"]').click() + + cy.wait(1000) + + cy.get('[data-testid="ui/message/4000001"]').should("to.exist") + cy.get(websiteTrait).should("have.value", "b") + cy.get(emailTrait).should("have.value", email) + cy.get(websiteTrait).clear() + cy.get(websiteTrait).type("https://www.ory.sh") + + cy.get('[name="passkey_register_trigger"]').click() + + cy.wait(1000) + + cy.getSession({ + expectAal: "aal1", + expectMethods: ["passkey"], + }).then((session) => { + expect(session.identity.traits.email).to.equal(email) + expect(session.identity.traits.website).to.equal("https://www.ory.sh") + }) + }) + + it("should pass transient_payload to webhook", () => { + testFlowWebhook( + (hooks) => + cy.setupHooks("registration", "after", "passkey", [ + ...hooks, + { hook: "session" }, + ]), + () => { + signup(registration, app) + }, + ) + }) + + it("should be able to login with registered account", () => { + const email = gen.email() + + signup(registration, app, email) + cy.logout() + cy.visit(login) + + cy.get('[name="passkey_login_trigger"]').click() + cy.wait(1000) + + cy.getSession({ + expectAal: "aal1", + expectMethods: ["passkey"], + }).then((session) => { + expect(session.identity.traits.email).to.equal(email) + expect(session.identity.traits.website).to.equal("https://www.ory.sh") + }) + }) + + it("should not be able to unlink last passkey", () => { + const email = gen.email() + signup(registration, app, email) + cy.visit(settings) + cy.get('[name="passkey_remove"]').should("have.attr", "disabled") + }) + + it("should be able to link password and use both methods for sign in", () => { + const email = gen.email() + const password = gen.password() + signup(registration, app, email) + cy.visit(settings) + cy.get('[name="passkey_remove"]').should("have.attr", "disabled") + cy.get('[name="password"]').type(password) + cy.get('[value="password"]').click() + cy.expectSettingsSaved() + cy.get('[name="passkey_remove"]').click() + cy.expectSettingsSaved() + cy.logout() + cy.visit(login) + + cy.get('[name="identifier"]').type(email) + cy.get('[name="password"]').type(password) + cy.get('[name="method"][value="password"]').click() + }) + + it("should be able to refresh", () => { + const email = gen.email() + signup(registration, app, email) + cy.visit(login + "?refresh=true") + cy.get('[name="identifier"][type="hidden"]').should("exist") + cy.get('[name="identifier"][type="input"]').should("not.exist") + cy.get('[name="password"]').should("not.exist") + cy.get('[value="password"]').should("not.exist") + cy.get('[name="passkey_login_trigger"]').click() + cy.wait(1000) + + cy.getSession({ + expectAal: "aal1", + expectMethods: ["passkey", "passkey"], + }).then((session) => { + expect(session.identity.traits.email).to.equal(email) + expect(session.identity.traits.website).to.equal("https://www.ory.sh") + }) + }) + + it("should not be able to use for MFA", () => { + const email = gen.email() + signup(registration, app, email) + cy.visit(login + "?aal=aal2") + cy.get('[value="passkey"]').should("not.exist") + cy.get('[name="passkey_login_trigger"]').should("not.exist") + }) + + it("should be able to add method later and try a variety of refresh flows", () => { + const email = gen.email() + const password = gen.password() + cy.visit(registration) + + const emailTrait = `${ + app === "express" ? `[data-testid="registration-flow"]` : "" + } [name="traits.email"]` + const websiteTrait = `${ + app === "express" ? `[data-testid="registration-flow"]` : "" + } [name="traits.website"]` + + cy.get(emailTrait).type(email) + cy.get('[name="password"]').type(password) + cy.get(websiteTrait).type("https://www.ory.sh") + cy.get('[value="password"]').click() + cy.location("pathname").should("not.contain", "/registration") + cy.getSession({ + expectAal: "aal1", + expectMethods: ["password"], + }) + + cy.visit(settings) + cy.get('[name="passkey_register_trigger"]').click() + cy.expectSettingsSaved() + + cy.visit(login + "?refresh=true") + cy.get('[name="password"]').should("exist") + cy.get('[name="passkey_login_trigger"]').click() + cy.wait(1000) + cy.location("pathname").should("not.contain", "/login") + cy.getSession({ + expectAal: "aal1", + expectMethods: ["password", "passkey", "passkey"], + }) + + cy.visit(login + "?refresh=true") + cy.get('[name="password"]').type(password) + cy.get('[value="password"]').click() + cy.getSession({ + expectAal: "aal1", + expectMethods: ["password", "passkey", "passkey", "password"], + }) + + cy.logout() + cy.visit(login) + + cy.get('[name="passkey_login_trigger"]').click() + cy.wait(1000) + cy.getSession({ + expectAal: "aal1", + expectMethods: ["passkey"], + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/passwordless/flows.spec.ts b/test/e2e/cypress/integration/profiles/passwordless/flows.spec.ts index 4b5bcfbfbdf8..adf4505dfaff 100644 --- a/test/e2e/cypress/integration/profiles/passwordless/flows.spec.ts +++ b/test/e2e/cypress/integration/profiles/passwordless/flows.spec.ts @@ -4,7 +4,7 @@ import { appPrefix, gen } from "../../../helpers" import { routes as express } from "../../../helpers/express" import { routes as react } from "../../../helpers/react" -import { testRegistrationWebhook } from "../../../helpers/webhook" +import { testFlowWebhook } from "../../../helpers/webhook" const signup = (registration: string, app: string, email = gen.email()) => { cy.visit(registration) @@ -115,7 +115,8 @@ context("Passwordless registration", () => { cy.get('[data-testid="ui/message/4000001"]').should("to.exist") cy.get(websiteTrait).should("have.value", "b") cy.get(emailTrait).should("have.value", email) - cy.get(websiteTrait).clear().type("https://www.ory.sh") + cy.get(websiteTrait).clear() + cy.get(websiteTrait).type("https://www.ory.sh") cy.clickWebAuthButton("register") cy.getSession({ expectAal: "aal1", @@ -127,14 +128,38 @@ context("Passwordless registration", () => { }) it("should pass transient_payload to webhook", () => { - testRegistrationWebhook( - (hooks) => cy.setupHooks("registration", "after", "webauthn", hooks), + testFlowWebhook( + (hooks) => + cy.setupHooks("registration", "after", "webauthn", [ + ...hooks, + { hook: "session" }, + ]), () => { signup(registration, app) }, ) }) + // I have no idea why this does not work in an E2E test. It works just fine manually. + xit("should use webauthn credential as passkey", () => { + const email = gen.email() + + signup(registration, app, email) + cy.logout() + cy.visit(login) + + cy.get('[name="passkey_login_trigger"]').click() + cy.wait(1000) + + cy.getSession({ + expectAal: "aal1", + expectMethods: ["passkey"], + }).then((session) => { + expect(session.identity.traits.email).to.equal(email) + expect(session.identity.traits.website).to.equal("https://www.ory.sh") + }) + }) + it("should be able to login with registered account", () => { const email = gen.email() @@ -164,7 +189,7 @@ context("Passwordless registration", () => { const email = gen.email() signup(registration, app, email) cy.visit(settings) - cy.get('[name="webauthn_remove"]').should("not.exist") + cy.get('[name="webauthn_remove"]').should("be.disabled") }) it("should be able to link password and use both methods for sign in", () => { @@ -172,7 +197,7 @@ context("Passwordless registration", () => { const password = gen.password() signup(registration, app, email) cy.visit(settings) - cy.get('[name="webauthn_remove"]').should("not.exist") + cy.get('[name="webauthn_remove"]').should("be.disabled") cy.get('[name="password"]').type(password) cy.get('[value="password"]').click() cy.expectSettingsSaved() @@ -291,7 +316,7 @@ context("Passwordless registration", () => { cy.get('[name="webauthn_login_trigger"]').should("not.exist") cy.visit(settings) - cy.get('[name="webauthn_remove"]').should("not.exist") + cy.get('[name="webauthn_remove"]').should("be.disabled") cy.get('[name="webauthn_register_displayname"]').type("key2") cy.clickWebAuthButton("register") cy.expectSettingsSaved() diff --git a/test/e2e/cypress/integration/profiles/recovery/code/success.spec.ts b/test/e2e/cypress/integration/profiles/recovery/code/success.spec.ts index a27bca61d967..baafe5a389c7 100644 --- a/test/e2e/cypress/integration/profiles/recovery/code/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/recovery/code/success.spec.ts @@ -180,7 +180,7 @@ context("Account Recovery With Code Success", () => { const identity = gen.identityWithWebsite() cy.registerApi(identity) - cy.visit(express.recovery + "?return_to=https://www.ory.sh/") + cy.visit(express.recovery + "?return_to=https://www.example.org/") cy.get("input[name='email']").type(identity.email) cy.get("button[value='code']").click() cy.get('[data-testid="ui/message/1060003"]').should( @@ -196,6 +196,6 @@ context("Account Recovery With Code Success", () => { cy.get('input[name="password"]').clear().type(gen.password()) cy.get('button[value="password"]').click() - cy.url().should("eq", "https://www.ory.sh/") + cy.url().should("eq", "https://www.example.org/") }) }) diff --git a/test/e2e/cypress/integration/profiles/recovery/link/success.spec.ts b/test/e2e/cypress/integration/profiles/recovery/link/success.spec.ts index fc4137200b59..abfa089375b8 100644 --- a/test/e2e/cypress/integration/profiles/recovery/link/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/recovery/link/success.spec.ts @@ -109,7 +109,10 @@ context("Account Recovery Success", () => { const identity = gen.identityWithWebsite() cy.registerApi(identity) - cy.recoverApi({ email: identity.email, returnTo: "https://www.ory.sh/" }) + cy.recoverApi({ + email: identity.email, + returnTo: "https://www.example.org/", + }) cy.recoverEmail({ expect: identity }) @@ -120,7 +123,7 @@ context("Account Recovery Success", () => { .clear() .type(gen.password()) cy.get('button[value="password"]').click() - cy.url().should("eq", "https://www.ory.sh/") + cy.url().should("eq", "https://www.example.org/") }) it("should recover even if already logged into another account", () => { diff --git a/test/e2e/cypress/integration/profiles/recovery/return-to/success.spec.ts b/test/e2e/cypress/integration/profiles/recovery/return-to/success.spec.ts index 0fa3f12a9524..af5ffa0acd1c 100644 --- a/test/e2e/cypress/integration/profiles/recovery/return-to/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/recovery/return-to/success.spec.ts @@ -63,7 +63,7 @@ context("Recovery with `return_to`", () => { } it("should return to the `return_to` url after successful account recovery and settings update", () => { - cy.visit(recovery + "?return_to=https://www.ory.sh/") + cy.visit(recovery + "?return_to=https://www.example.org/") doRecovery() cy.get('[data-testid="ui/message/1060001"]', { timeout: 30000 }).should( @@ -80,7 +80,7 @@ context("Recovery with `return_to`", () => { .type(newPassword) cy.get('button[value="password"]').click() - cy.location("hostname").should("eq", "www.ory.sh") + cy.location("hostname").should("eq", "www.example.org") }) it("should return to the `return_to` url even with mfa enabled after successful account recovery and settings update", () => { @@ -108,7 +108,7 @@ context("Recovery with `return_to`", () => { cy.logout() cy.clearAllCookies() - cy.visit(recovery + "?return_to=https://www.ory.sh/") + cy.visit(recovery + "?return_to=https://www.example.org/") doRecovery() cy.shouldShow2FAScreen() @@ -122,7 +122,7 @@ context("Recovery with `return_to`", () => { .clear() .type(newPassword) cy.get('button[value="password"]').click() - cy.location("hostname").should("eq", "www.ory.sh") + cy.location("hostname").should("eq", "www.example.org") }) }) }) diff --git a/test/e2e/cypress/integration/profiles/two-steps/registration/code.spec.ts b/test/e2e/cypress/integration/profiles/two-steps/registration/code.spec.ts new file mode 100644 index 000000000000..bffafb36ee03 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/two-steps/registration/code.spec.ts @@ -0,0 +1,382 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { Session } from "@ory/kratos-client" +import { gen, MOBILE_URL } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +const Selectors = { + mobile: { + identifier: "[data-testid='field/identifier']", + recoveryEmail: "[data-testid='field/email']", + email: "[data-testid='traits.email']", + email2: "[data-testid='traits.email2']", + website: "[data-testid='traits.website']", + username: "[data-testid='traits.username']", + code: "[data-testid='field/code'] input", + recoveryCode: "[data-testid='code']", + submitCode: "[data-testid='field/method/code']", + resendCode: "[data-testid='field/resend/code']", + credentialSelection: "[data-testid='field/screen/credential-selection']", + submitRecovery: "[data-testid='field/method/code']", + codeHiddenMethod: "[data-testid='field/method/code']", + }, + express: { + identifier: "[data-testid='login-flow-code'] input[name='identifier']", + recoveryEmail: "input[name=email]", + email: "[data-testid='node/input/traits.email'] input[name='traits.email']", + email2: + "[data-testid='node/input/traits.email2'] input[name='traits.email2']", + website: + "[data-testid='node/input/traits.website'] [name='traits.website']", + username: + "[data-testid='node/input/traits.username'] input[name='traits.username']", + code: "input[name='code']", + recoveryCode: "input[name=code]", + submitRecovery: "button[name=method][value=code]", + submitCode: "button[name='method'][value='code']", + resendCode: "button[name='resend'][value='code']", + codeHiddenMethod: "input[name='method'][value='code'][type='hidden']", + credentialSelection: "[name='screen'][value='credential-selection']", + }, + react: { + identifier: "input[name='identifier']", + recoveryEmail: "input[name=email]", + email: "input[name='traits.email']", + email2: "input[name='traits.email2']", + website: "[name='traits.website']", + username: "input[name='traits.username']", + code: "input[name='code']", + recoveryCode: "input[name=code]", + submitRecovery: "button[name=method][value=code]", + submitCode: "button[name='method'][value='code']", + resendCode: "button[name='resend'][value='code']", + codeHiddenMethod: "input[name='method'][value='code'][type='hidden']", + credentialSelection: "[name='screen'][value='credential-selection']", + }, +} + +context("Registration success with code method", () => { + ;[ + { + route: express.registration, + login: express.login, + recovery: express.recovery, + app: "express" as "express", + profile: "two-steps", + }, + { + route: react.registration, + login: react.login, + recovery: react.recovery, + app: "react" as "react", + profile: "two-steps", + }, + { + route: MOBILE_URL + "/Registration", + login: MOBILE_URL + "/Login", + recovery: MOBILE_URL + "/Recovery", + app: "mobile" as "mobile", + profile: "two-steps", + }, + ].forEach(({ route, login, recovery, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + if (app !== "mobile") { + cy.proxy(app) + } + }) + + beforeEach(() => { + cy.deleteMail({ atLeast: 0 }) + cy.clearAllCookies() + cy.visit(route) + }) + + it("should be able to resend the registration code", () => { + const email = gen.email() + const website = "https://www.example.org/" + + cy.get(Selectors[app]["email"]).type(email) + cy.get(Selectors[app]["website"]).type(website) + + cy.submitProfileForm(app) + cy.submitCodeForm(app) + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + cy.getRegistrationCodeFromEmail(email).then((code) => + cy.wrap(code).as("code1"), + ) + + // cy.get(Selectors[app]["email"]).should("have.value", email) + cy.get(Selectors[app]["codeHiddenMethod"]).should("exist") + cy.get(Selectors[app]["resendCode"]).click() + + cy.getRegistrationCodeFromEmail(email).then((code) => { + cy.wrap(code).as("code2") + }) + + cy.get("@code1").then((code1) => { + // previous code should not work + cy.get(Selectors[app]["code"]).clear() + cy.get(Selectors[app]["code"]).type(code1.toString()) + + cy.submitCodeForm(app) + cy.get('[data-testid="ui/message/4040003"]').should( + "contain.text", + "The registration code is invalid or has already been used. Please try again.", + ) + }) + + // Navigate back and forth again + cy.get(Selectors[app]["credentialSelection"]).click() + cy.submitCodeForm(app) + + cy.getRegistrationCodeFromEmail(email).then((code) => { + cy.wrap(code).as("code2") + }) + + cy.get("@code2").then((code2) => { + cy.get(Selectors[app]["code"]).clear() + cy.get(Selectors[app]["code"]).type(code2.toString()) + cy.submitCodeForm(app) + }) + + if (app === "express") { + cy.url().should("match", /\/welcome/) + } else { + cy.get('[data-testid="session-content"]').should("contain", email) + } + + if (app === "mobile") { + cy.get('[data-testid="session-token"]').should("not.be.empty") + cy.get('[data-testid="session-token"]').then((token) => { + cy.getSession({ + expectAal: "aal1", + expectMethods: ["code"], + token: token.text(), + }).then((session) => { + cy.wrap(session).as("session") + }) + }) + } else { + cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then( + (session) => { + cy.wrap(session).as("session") + }, + ) + } + + cy.get("@session").then(({ identity }) => { + expect(identity.id).to.not.be.empty + expect(identity.traits.email).to.equal(email) + }) + }) + + it("should sign up and be logged in with session hook", () => { + const email = gen.email() + const website = "https://www.example.org/" + + cy.get(Selectors[app]["email"]).type(email) + cy.get(Selectors[app]["website"]).type(website) + cy.submitProfileForm(app) + + cy.submitCodeForm(app) + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + cy.getRegistrationCodeFromEmail(email).should((code) => { + cy.get(Selectors[app]["code"]).type(code) + cy.get(Selectors[app]["submitCode"]).click() + }) + + if (app === "mobile") { + cy.get('[data-testid="session-token"]').then((token) => { + cy.getSession({ + expectAal: "aal1", + expectMethods: ["code"], + token: token.text(), + }).then((session) => { + cy.wrap(session).as("session") + }) + }) + + cy.get('[data-testid="session-content"]').should("contain", email) + cy.get('[data-testid="session-token"]').should("not.be.empty") + } else { + cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then( + (session) => { + cy.wrap(session).as("session") + }, + ) + } + + cy.get("@session").then(({ identity }) => { + expect(identity.id).to.not.be.empty + expect(identity.traits.email).to.equal(email) + }) + }) + + it("should be able to sign up without session hook", () => { + cy.setPostCodeRegistrationHooks([]) + const email = gen.email() + const website = "https://www.example.org/" + + cy.get(Selectors[app]["email"]).type(email) + cy.get(Selectors[app]["website"]).type(website) + cy.submitProfileForm(app) + + cy.submitCodeForm(app) + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + cy.getRegistrationCodeFromEmail(email).should((code) => { + cy.get(Selectors[app]["code"]).type(code) + cy.get(Selectors[app]["submitCode"]).click() + }) + + cy.visit(login) + cy.get(Selectors[app]["identifier"]).type(email) + cy.get(Selectors[app]["submitCode"]).click() + + cy.getLoginCodeFromEmail(email).then((code) => { + cy.get(Selectors[app]["code"]).type(code) + cy.get(Selectors[app]["submitCode"]).click() + }) + + if (app === "mobile") { + cy.get('[data-testid="session-token"]').then((token) => { + cy.getSession({ + expectAal: "aal1", + expectMethods: ["code"], + token: token.text(), + }).then((session) => { + cy.wrap(session).as("session") + }) + }) + + cy.get('[data-testid="session-content"]').should("contain", email) + cy.get('[data-testid="session-token"]').should("not.be.empty") + } else { + cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then( + (session) => { + cy.wrap(session).as("session") + }, + ) + } + + cy.get("@session").then(({ identity }) => { + expect(identity.id).to.not.be.empty + expect(identity.traits.email).to.equal(email) + }) + }) + + // Try keep this test as the last one, as it updates the identity schema. + it("should be able to use multiple identifiers to signup with and sign in to", () => { + cy.setPostCodeRegistrationHooks([ + { + hook: "session", + }, + ]) + + // Setup complex schema + cy.setIdentitySchema( + "file://test/e2e/profiles/code/identity.complex.traits.schema.json", + ) + + cy.visit(route) + + cy.get(Selectors[app]["username"]).type(Math.random().toString(36)) + + const email = gen.email() + cy.get(Selectors[app]["email"]).type(email) + + const email2 = gen.email() + cy.get(Selectors[app]["email2"]).type(email2) + + cy.submitProfileForm(app) + cy.submitCodeForm(app) + cy.get('[data-testid="ui/message/1040005"]').should( + "contain", + "An email containing a code has been sent to the email address you provided", + ) + + // intentionally use email 1 to sign up for the account + cy.getRegistrationCodeFromEmail(email, { expectedCount: 1 }).should( + (code) => { + cy.get(Selectors[app]["code"]).type(code) + cy.get(Selectors[app]["submitCode"]).click() + }, + ) + + if (app === "mobile") { + cy.visit(MOBILE_URL + "/Home") + cy.get('*[data-testid="logout"]').click() + } else { + cy.logout() + } + + // There are verification emails from the registration process in the inbox that we need to deleted + // for the assertions below to pass. + cy.deleteMail({ atLeast: 1 }) + + // Attempt to sign in with email 2 (should fail) + cy.visit(login) + cy.get(Selectors[app]["identifier"]).type(email2) + + cy.get(Selectors[app]["submitCode"]).click() + + cy.getLoginCodeFromEmail(email2, { + expectedCount: 1, + }).should((code) => { + cy.get(Selectors[app]["code"]).type(code) + cy.get(Selectors[app]["submitCode"]).click() + }) + + if (app === "mobile") { + cy.get('[data-testid="session-token"]').then((token) => { + cy.getSession({ + expectAal: "aal1", + expectMethods: ["code"], + token: token.text(), + }).then((session) => { + cy.wrap(session).as("session") + }) + }) + + cy.get('[data-testid="session-content"]').should("contain", email) + cy.get('[data-testid="session-token"]').should("not.be.empty") + } else { + cy.getSession({ expectAal: "aal1", expectMethods: ["code"] }).then( + (session) => { + cy.wrap(session).as("session") + }, + ) + } + + cy.get("@session").then(({ identity }) => { + expect(identity.id).to.not.be.empty + expect(identity.verifiable_addresses).to.have.length(2) + expect( + identity.verifiable_addresses.filter((v) => v.value === email)[0] + .status, + ).to.equal("completed") + expect( + identity.verifiable_addresses.filter((v) => v.value === email2)[0] + .status, + ).to.equal("completed") + expect(identity.traits.email).to.equal(email) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/two-steps/registration/oidc.spec.ts b/test/e2e/cypress/integration/profiles/two-steps/registration/oidc.spec.ts new file mode 100644 index 000000000000..ca2d5e57078a --- /dev/null +++ b/test/e2e/cypress/integration/profiles/two-steps/registration/oidc.spec.ts @@ -0,0 +1,220 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, gen, website } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" +import { testFlowWebhook } from "../../../../helpers/webhook" + +context("Social Sign Up Successes", () => { + ;[ + { + login: react.login, + registration: react.registration, + app: "react" as "react", + profile: "two-steps", + }, + { + login: express.login, + registration: express.registration, + app: "express" as "express", + profile: "two-steps", + }, + ].forEach(({ registration, login, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + beforeEach(() => { + cy.clearAllCookies() + cy.visit(registration) + cy.setIdentitySchema( + "file://test/e2e/profiles/oidc/identity.traits.schema.json", + ) + }) + + const shouldSession = (email) => (session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.traits.website).to.equal(website) + expect(identity.traits.email).to.equal(email) + } + + it("should be able to sign up with incomplete data and finally be signed in", () => { + const email = gen.email() + + cy.registerOidc({ + app, + email, + expectSession: false, + route: registration, + }) + + cy.get("#registration-password").should("not.exist") + cy.get(appPrefix(app) + '[name="traits.email"]').should( + "have.value", + email, + ) + cy.get('[data-testid="ui/message/4000002"]').should( + "contain.text", + "Property website is missing", + ) + + cy.get('[name="traits.consent"][type="checkbox"]') + .siblings("label") + .click() + cy.get('[name="traits.newsletter"][type="checkbox"]') + .siblings("label") + .click() + cy.get('[name="traits.website"]').type("http://s") + + cy.get('[name="provider"]') + .should("have.length", 1) + .should("have.value", "hydra") + .should("contain.text", "Continue") + .click() + + cy.get("#registration-password").should("not.exist") + cy.get('[name="traits.email"]').should("have.value", email) + cy.get('[name="traits.website"]').should("have.value", "http://s") + cy.get('[data-testid="ui/message/4000003"]').should( + "contain.text", + "length must be >= 10", + ) + cy.get('[name="traits.website"]') + .should("have.value", "http://s") + .clear() + .type(website) + + cy.get('[name="traits.consent"]').should("be.checked") + cy.get('[name="traits.newsletter"]').should("be.checked") + + cy.triggerOidc(app) + + cy.location("pathname").should((loc) => { + expect(loc).to.be.oneOf(["/welcome", "/", "/sessions"]) + }) + + cy.getSession().should((session) => { + shouldSession(email)(session) + expect(session.identity.traits.consent).to.equal(true) + }) + }) + + it("should pass transient_payload to webhook", () => { + testFlowWebhook( + (hooks) => + cy.setupHooks("registration", "after", "oidc", [ + ...hooks, + { hook: "session" }, + ]), + () => { + const email = gen.email() + cy.registerOidc({ + app, + email, + website, + route: registration, + }) + cy.getSession().should(shouldSession(email)) + }, + ) + }) + + it("should be able to sign up with complete data", () => { + const email = gen.email() + + cy.registerOidc({ app, email, website, route: registration }) + cy.getSession().should(shouldSession(email)) + }) + + it("should be able to convert a sign up flow to a sign in flow", () => { + const email = gen.email() + + cy.registerOidc({ app, email, website, route: registration }) + cy.logout() + cy.noSession() + cy.visit(registration) + cy.triggerOidc(app) + + cy.location("pathname").should((path) => { + expect(path).to.oneOf(["/", "/welcome", "/sessions"]) + }) + + cy.getSession().should(shouldSession(email)) + }) + + it("should be able to convert a sign in flow to a sign up flow", () => { + cy.setIdentitySchema( + "file://test/e2e/profiles/oidc/identity-required.traits.schema.json", + ) + + const email = gen.email() + cy.visit(login) + cy.triggerOidc(app) + + cy.get("#username").clear().type(email) + cy.get("#remember").click() + cy.get("#accept").click() + cy.get('[name="scope"]').each(($el) => cy.wrap($el).click()) + cy.get("#remember").click() + cy.get("#accept").click() + + cy.get('[data-testid="ui/message/4000002"]').should( + "contain.text", + "Property website is missing", + ) + cy.get('[name="traits.website"]').type("http://s") + + cy.triggerOidc(app) + + cy.get('[data-testid="ui/message/4000003"]').should( + "contain.text", + "length must be >= 10", + ) + cy.get('[name="traits.requirednested"]').should("not.exist") + cy.get('[name="traits.requirednested.a"]').siblings("label").click() + cy.get('[name="traits.consent"]').siblings("label").click() + cy.get('[name="traits.website"]') + .should("have.value", "http://s") + .clear() + .type(website) + cy.triggerOidc(app) + + cy.location("pathname").should("not.contain", "/registration") + + cy.getSession().should(shouldSession(email)) + }) + + it("should be able to sign up with redirects", () => { + const email = gen.email() + cy.registerOidc({ + app, + email, + website, + route: registration + "?return_to=https://www.example.org/", + }) + cy.location("href").should("eq", "https://www.example.org/") + cy.logout() + }) + + it("should be able to register with upstream parameters", () => { + const email = gen.email() + cy.intercept("GET", "**/oauth2/auth*").as("getHydraRegistration") + + cy.visit(registration + "?return_to=https://www.example.org/") + + cy.addInputElement("form", "upstream_parameters.login_hint", email) + + cy.triggerOidc(app) + + // once a request to getHydraRegistration responds, 'cy.wait' will resolve + cy.wait("@getHydraRegistration") + .its("request.url") + .should("include", "login_hint=" + encodeURIComponent(email)) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/two-steps/registration/password.spec.ts b/test/e2e/cypress/integration/profiles/two-steps/registration/password.spec.ts new file mode 100644 index 000000000000..8621b7a69483 --- /dev/null +++ b/test/e2e/cypress/integration/profiles/two-steps/registration/password.spec.ts @@ -0,0 +1,115 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { appPrefix, APP_URL, gen } from "../../../../helpers" +import { routes as express } from "../../../../helpers/express" +import { routes as react } from "../../../../helpers/react" + +context("Registration success with two-step signup", () => { + ;[ + { + route: express.registration, + app: "express" as "express", + profile: "two-steps", + }, + { + route: react.registration, + app: "react" as "react", + profile: "two-steps", + }, + ].forEach(({ route, profile, app }) => { + describe(`for app ${app}`, () => { + before(() => { + cy.useConfigProfile(profile) + cy.proxy(app) + }) + + beforeEach(() => { + cy.deleteMail() + cy.clearAllCookies() + cy.visit(route) + cy.enableVerification() + if (app === "express") { + cy.enableVerificationUIAfterRegistration("password") + } + }) + + it("should sign up and be logged in", () => { + const email = gen.email() + const password = gen.password() + const website = "https://www.example.org/" + + // Fill out step one forms + cy.get(appPrefix(app) + 'input[name="traits"]').should("not.exist") + cy.get('input[name="traits.email"]').type("someone@foo") + cy.get('input[name="traits.website"]').type(website) + cy.get('[name="method"][value="profile"]').click() + + // navigate back, fill traits again + cy.get('[name="screen"][value="previous"]').click() + cy.get('input[name="traits.email"]').type( + "{selectall}{backspace}" + email, + ) + cy.get('[name="method"][value="profile"]').click() + + // Fill out step two forms + cy.get('input[name="password"]').type(password) + cy.get('[name="method"][value="password"]').click() + + if (app === "express") { + cy.get('a[href*="sessions"]').click() + } + cy.get("pre").should("contain.text", email) + + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.schema_id).to.equal("default") + expect(identity.schema_url).to.equal(`${APP_URL}/schemas/ZGVmYXVsdA`) + expect(identity.traits.website).to.equal(website) + expect(identity.traits.email).to.equal(email) + }) + }) + + it("should handle form errors", () => { + const email = gen.email() + const password = gen.password() + const websiteTooShort = "a://b" + const website = "https://www.example.com" + + // Fill out step one forms + cy.get(appPrefix(app) + 'input[name="traits"]').should("not.exist") + cy.get('input[name="traits.email"]').type(email) + cy.get('input[name="traits.website"]').type(websiteTooShort) + cy.get('[name="method"][value="profile"]').click() + + // Assert form errors + cy.get('[data-testid="ui/message/4000003"]').should("to.exist") + + // Enter correct value + cy.get('input[name="traits.website"]').type( + "{selectall}{backspace}" + website, + ) + cy.get('[name="method"][value="profile"]').click() + + // Fill out step two forms + cy.get('input[name="password"]').type(password) + cy.get('[name="method"][value="password"]').click() + + if (app === "express") { + cy.get('a[href*="sessions"]').click() + } + cy.get("pre").should("contain.text", email) + + cy.getSession().should((session) => { + const { identity } = session + expect(identity.id).to.not.be.empty + expect(identity.schema_id).to.equal("default") + expect(identity.schema_url).to.equal(`${APP_URL}/schemas/ZGVmYXVsdA`) + expect(identity.traits.website).to.equal(website) + expect(identity.traits.email).to.equal(email) + }) + }) + }) + }) +}) diff --git a/test/e2e/cypress/integration/profiles/webhoooks/login/success.spec.ts b/test/e2e/cypress/integration/profiles/webhoooks/login/success.spec.ts index b346c9475c05..8e9c00e7ecbb 100644 --- a/test/e2e/cypress/integration/profiles/webhoooks/login/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/webhoooks/login/success.spec.ts @@ -3,6 +3,7 @@ import { APP_URL, appPrefix, gen, website } from "../../../../helpers" import { routes as express } from "../../../../helpers/express" +import { testFlowWebhook } from "../../../../helpers/webhook" describe("Basic email profile with succeeding login flows with webhooks", () => { const email = gen.email() @@ -43,6 +44,17 @@ describe("Basic email profile with succeeding login flows with webhooks", () => expect(identity.traits.email).to.equal(email) }) }) + + it("should pass transient_payload to webhook", () => { + testFlowWebhook( + (h) => cy.setupHooks("login", "after", "password", h), + () => { + cy.get(`${appPrefix(app)}input[name="identifier"]`).type(email) + cy.get('input[name="password"]').type(password) + cy.submitPasswordForm() + }, + ) + }) }) }) }) diff --git a/test/e2e/cypress/integration/profiles/webhoooks/registration/success.spec.ts b/test/e2e/cypress/integration/profiles/webhoooks/registration/success.spec.ts index a2dcd123923c..ebea31c9c36b 100644 --- a/test/e2e/cypress/integration/profiles/webhoooks/registration/success.spec.ts +++ b/test/e2e/cypress/integration/profiles/webhoooks/registration/success.spec.ts @@ -3,7 +3,7 @@ import { appPrefix, APP_URL, gen } from "../../../../helpers" import { routes as express } from "../../../../helpers/express" -import { testRegistrationWebhook } from "../../../../helpers/webhook" +import { testFlowWebhook } from "../../../../helpers/webhook" context("Registration success with email profile with webhooks", () => { ;[ @@ -86,18 +86,15 @@ context("Registration success with email profile with webhooks", () => { }) it("should pass transient_payload to webhook", () => { - testRegistrationWebhook( - cy.setPostPasswordRegistrationHooks.bind(cy), - () => { - const email = gen.email() - const password = gen.password() + testFlowWebhook(cy.setPostPasswordRegistrationHooks.bind(cy), () => { + const email = gen.email() + const password = gen.password() - cy.get('input[name="traits.email"]').type(email) - cy.get('input[name="password"]').type(password) + cy.get('input[name="traits.email"]').type(email) + cy.get('input[name="password"]').type(password) - cy.submitPasswordForm() - }, - ) + cy.submitPasswordForm() + }) }) it("should sign up and modify the identity", () => { diff --git a/test/e2e/cypress/support/commands.ts b/test/e2e/cypress/support/commands.ts index 7ef6260f4b78..89b5c7cb15c5 100644 --- a/test/e2e/cypress/support/commands.ts +++ b/test/e2e/cypress/support/commands.ts @@ -17,7 +17,7 @@ import { import dayjs from "dayjs" import YAML from "yamljs" import { MailMessage, Strategy } from "." -import { OryKratosConfiguration } from "./config" +import { OryKratosConfiguration } from "../../shared/config" import { UiNode } from "@ory/kratos-client" import { ConfigBuilder } from "./configHelpers" @@ -429,7 +429,7 @@ Cypress.Commands.add( f.group === "default" && "name" in f.attributes && f.attributes.name === "traits.email", - ).attributes.value, + )?.attributes.value, ).to.eq(email) return cy @@ -845,7 +845,7 @@ Cypress.Commands.add( if (expectSession) { // for some reason react flakes here although the login succeeded and there should be a session it fails if (app === "react") { - cy.wait(2000) // adding arbitrary wait here. not sure if there is a better way in this case + cy.wait(500) // adding arbitrary wait here. not sure if there is a better way in this case } cy.getSession() } else { @@ -922,8 +922,9 @@ Cypress.Commands.add("logout", () => { const c = cookies.find( ({ name }) => name.indexOf("ory_kratos_session") > -1, ) - expect(c).to.not.be.undefined - cy.clearCookie(c.name) + if (c) { + cy.clearCookie(c.name) + } }) cy.noSession() }) @@ -1348,9 +1349,14 @@ Cypress.Commands.add("submitPasswordForm", () => { cy.get('[name="method"][value="password"]:disabled').should("not.exist") }) -Cypress.Commands.add("submitProfileForm", () => { - cy.get('[name="method"][value="profile"]').click() - cy.get('[name="method"][value="profile"]:disabled').should("not.exist") +Cypress.Commands.add("submitProfileForm", (app?: string) => { + if (app === "mobile") { + cy.get('[data-testid="field/method/profile"]').click() + cy.get('[data-testid="field/method/profile"]:disabled').should("not.exist") + } else { + cy.get('[name="method"][value="profile"]').click() + cy.get('[name="method"][value="profile"]:disabled').should("not.exist") + } }) Cypress.Commands.add("submitCodeForm", (app) => { diff --git a/test/e2e/cypress/support/configHelpers.ts b/test/e2e/cypress/support/configHelpers.ts index c8ccf05b70d2..566bb475fa58 100644 --- a/test/e2e/cypress/support/configHelpers.ts +++ b/test/e2e/cypress/support/configHelpers.ts @@ -1,7 +1,7 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import { OryKratosConfiguration } from "./config" +import { OryKratosConfiguration } from "../../shared/config" export class ConfigBuilder { constructor(readonly config: OryKratosConfiguration) {} @@ -25,6 +25,11 @@ export class ConfigBuilder { return this } + public disableCodeMfa() { + this.config.selfservice.methods.code.mfa_enabled = false + return this + } + public enableRecovery() { if (!this.config.selfservice.flows.recovery) { this.config.selfservice.flows.recovery = {} @@ -132,6 +137,13 @@ export class ConfigBuilder { this.config.session.whoami.required_aal = "aal1" return this } + + public useHighestAvailable() { + this.config.selfservice.flows.settings.required_aal = "highest_available" + this.config.session.whoami.required_aal = "highest_available" + return this + } + public enableCode() { this.config.selfservice.methods.code.enabled = true return this diff --git a/test/e2e/cypress/support/index.d.ts b/test/e2e/cypress/support/index.d.ts index 3fef679ec098..a6a7120937de 100644 --- a/test/e2e/cypress/support/index.d.ts +++ b/test/e2e/cypress/support/index.d.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Session as KratosSession } from "@ory/kratos-client" -import { OryKratosConfiguration } from "./config" +import { OryKratosConfiguration } from "../../shared/config" import { ConfigBuilder } from "./configHelpers" export interface MailMessage { @@ -38,7 +38,12 @@ declare global { getSession(opts?: { expectAal?: "aal2" | "aal1" expectMethods?: Array< - "password" | "webauthn" | "lookup_secret" | "totp" | "code" + | "password" + | "webauthn" + | "lookup_secret" + | "totp" + | "code" + | "passkey" > token?: string }): Chainable @@ -186,7 +191,7 @@ declare global { | "verification" | "settings", phase: "before" | "after", - kind: "password" | "webauthn" | "oidc" | "code", + kind: "password" | "webauthn" | "oidc" | "code" | "passkey", hooks: Array<{ hook: string; config?: any }>, ): Chainable @@ -359,7 +364,7 @@ declare global { /** * Submits a profile form by clicking the button with method=profile */ - submitProfileForm(): Chainable + submitProfileForm(app?: "mobile" | "express" | "react"): Chainable /** * Submits a code form by clicking the button with method=code diff --git a/test/e2e/cypress/tsconfig.json b/test/e2e/cypress/tsconfig.json index 6042605a6887..dd9b96adae48 100644 --- a/test/e2e/cypress/tsconfig.json +++ b/test/e2e/cypress/tsconfig.json @@ -2,15 +2,9 @@ "compilerOptions": { "baseUrl": "../../../node_modules", "target": "es5", - "lib": [ - "es2015", - "dom" - ], + "lib": ["es2015", "dom"], "types": ["cypress", "node"], - "esModuleInterop": true, + "esModuleInterop": true }, - "include": [ - "**/*.ts", - "support/index.ts", - ], + "include": ["**/*.ts", "support/index.ts", "../shared/config.d.ts"] } diff --git a/test/e2e/mock/httptarget/go.mod b/test/e2e/mock/httptarget/go.mod new file mode 100644 index 000000000000..a82d636fb196 --- /dev/null +++ b/test/e2e/mock/httptarget/go.mod @@ -0,0 +1,10 @@ +module github.com/ory/mock + +go 1.23.1 + +require ( + github.com/julienschmidt/httprouter v1.3.0 + github.com/ory/graceful v0.1.3 +) + +require github.com/pkg/errors v0.9.1 // indirect diff --git a/test/e2e/mock/httptarget/go.sum b/test/e2e/mock/httptarget/go.sum new file mode 100644 index 000000000000..f6d2794d572f --- /dev/null +++ b/test/e2e/mock/httptarget/go.sum @@ -0,0 +1,9 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/ory/graceful v0.1.3/go.mod h1:4zFz687IAF7oNHHiB586U4iL+/4aV09o/PYLE34t2bA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/test/e2e/package-lock.json b/test/e2e/package-lock.json index f7f7bd88539a..f6452591bda9 100644 --- a/test/e2e/package-lock.json +++ b/test/e2e/package-lock.json @@ -8,13 +8,14 @@ "name": "@ory/kratos-e2e-suite", "version": "0.0.1", "dependencies": { - "@faker-js/faker": "7.6.0", + "@faker-js/faker": "8.4.1", "async-retry": "1.3.3", - "mailhog": "4.16.0" + "mailhog": "4.16.0", + "promise-retry": "^2.0.1" }, "devDependencies": { - "@ory/kratos-client": "0.0.0-next.8d3b018594f7", - "@playwright/test": "1.34.0", + "@ory/kratos-client": "1.2.0", + "@playwright/test": "1.44.1", "@types/async-retry": "1.4.5", "@types/node": "16.9.6", "@types/yamljs": "0.2.31", @@ -98,12 +99,19 @@ } }, "node_modules/@faker-js/faker": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", - "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", "engines": { - "node": ">=14.0.0", - "npm": ">=6.0.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" } }, "node_modules/@hapi/hoek": { @@ -128,12 +136,13 @@ "dev": true }, "node_modules/@ory/kratos-client": { - "version": "0.0.0-next.8d3b018594f7", - "resolved": "https://registry.npmjs.org/@ory/kratos-client/-/kratos-client-0.0.0-next.8d3b018594f7.tgz", - "integrity": "sha512-TkpjBo6Z6UUEJIJCR2EDdpKVDNgQHzwDWZbOjz3xTOUoGipMBykvIfluP58Jwkpt2rIXUkt9+L+u1mFFvD/tqA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ory/kratos-client/-/kratos-client-1.2.0.tgz", + "integrity": "sha512-W6jFkVEjnoq5ylGOvYOOaNvEZ1cGSEN/YJsZTcBVye81nQtW5R7QWClvNsJVD1LjwgWGMVKWglrFlfHvvkKnmg==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "axios": "^0.21.1" + "axios": "^1.6.1" } }, "node_modules/@otplib/core": { @@ -184,22 +193,19 @@ } }, "node_modules/@playwright/test": { - "version": "1.34.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.34.0.tgz", - "integrity": "sha512-GIALJVODOIrMflLV54H3Cow635OfrTwOu24ZTDyKC66uchtFX2NcCRq83cLdakMjZKYK78lODNLQSYBj2OgaTw==", + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz", + "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@types/node": "*", - "playwright-core": "1.34.0" + "playwright": "1.44.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "fsevents": "2.3.2" + "node": ">=16" } }, "node_modules/@sideway/address": { @@ -534,14 +540,39 @@ "dev": true }, "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dev": true, + "license": "MIT", "dependencies": { - "follow-redirects": "^1.14.0" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1121,6 +1152,11 @@ "node": ">=8.6" } }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" + }, "node_modules/es5-ext": { "version": "0.10.62", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", @@ -1304,9 +1340,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { @@ -1314,6 +1350,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -1373,6 +1410,7 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2336,13 +2374,36 @@ "node": ">=0.10.0" } }, + "node_modules/playwright": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", + "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.44.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, "node_modules/playwright-core": { - "version": "1.34.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.0.tgz", - "integrity": "sha512-fMUY1+iR6kYbJF/EsOOqzBA99ZHXbw9sYPNjwA4X/oV0hVF/1aGlWYBGPVUEqxBkGANDKMziYoOdKGU5DIP5Gg==", + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", + "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, "engines": { - "node": ">=14" + "node": ">=16" } }, "node_modules/prettier": { @@ -2381,6 +2442,26 @@ "node": ">= 0.6.0" } }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "engines": { + "node": ">= 4" + } + }, "node_modules/proxy-from-env": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", @@ -3091,9 +3172,9 @@ } }, "@faker-js/faker": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", - "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==" + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==" }, "@hapi/hoek": { "version": "9.3.0", @@ -3117,12 +3198,12 @@ "dev": true }, "@ory/kratos-client": { - "version": "0.0.0-next.8d3b018594f7", - "resolved": "https://registry.npmjs.org/@ory/kratos-client/-/kratos-client-0.0.0-next.8d3b018594f7.tgz", - "integrity": "sha512-TkpjBo6Z6UUEJIJCR2EDdpKVDNgQHzwDWZbOjz3xTOUoGipMBykvIfluP58Jwkpt2rIXUkt9+L+u1mFFvD/tqA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ory/kratos-client/-/kratos-client-1.2.0.tgz", + "integrity": "sha512-W6jFkVEjnoq5ylGOvYOOaNvEZ1cGSEN/YJsZTcBVye81nQtW5R7QWClvNsJVD1LjwgWGMVKWglrFlfHvvkKnmg==", "dev": true, "requires": { - "axios": "^0.21.1" + "axios": "^1.6.1" } }, "@otplib/core": { @@ -3173,14 +3254,12 @@ } }, "@playwright/test": { - "version": "1.34.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.34.0.tgz", - "integrity": "sha512-GIALJVODOIrMflLV54H3Cow635OfrTwOu24ZTDyKC66uchtFX2NcCRq83cLdakMjZKYK78lODNLQSYBj2OgaTw==", + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz", + "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==", "dev": true, "requires": { - "@types/node": "*", - "fsevents": "2.3.2", - "playwright-core": "1.34.0" + "playwright": "1.44.1" } }, "@sideway/address": { @@ -3459,12 +3538,33 @@ "dev": true }, "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "dev": true, "requires": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + } } }, "balanced-match": { @@ -3914,6 +4014,11 @@ "ansi-colors": "^4.1.1" } }, + "err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" + }, "es5-ext": { "version": "0.10.62", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", @@ -4066,9 +4171,9 @@ } }, "follow-redirects": { - "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true }, "forever-agent": { @@ -4825,10 +4930,20 @@ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, + "playwright": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", + "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.44.1" + } + }, "playwright-core": { - "version": "1.34.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.0.tgz", - "integrity": "sha512-fMUY1+iR6kYbJF/EsOOqzBA99ZHXbw9sYPNjwA4X/oV0hVF/1aGlWYBGPVUEqxBkGANDKMziYoOdKGU5DIP5Gg==", + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", + "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", "dev": true }, "prettier": { @@ -4849,6 +4964,22 @@ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "dev": true }, + "promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "dependencies": { + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==" + } + } + }, "proxy-from-env": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", diff --git a/test/e2e/package.json b/test/e2e/package.json index d4106cbb8aa1..05b04db6880a 100644 --- a/test/e2e/package.json +++ b/test/e2e/package.json @@ -11,13 +11,14 @@ "wait-on": "wait-on" }, "dependencies": { - "@faker-js/faker": "7.6.0", + "@faker-js/faker": "8.4.1", "async-retry": "1.3.3", - "mailhog": "4.16.0" + "mailhog": "4.16.0", + "promise-retry": "^2.0.1" }, "devDependencies": { - "@ory/kratos-client": "0.0.0-next.8d3b018594f7", - "@playwright/test": "1.34.0", + "@ory/kratos-client": "1.2.0", + "@playwright/test": "1.44.1", "@types/async-retry": "1.4.5", "@types/node": "16.9.6", "@types/yamljs": "0.2.31", diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts index 71a67dfd8795..a3f81c73060d 100644 --- a/test/e2e/playwright.config.ts +++ b/test/e2e/playwright.config.ts @@ -4,7 +4,7 @@ import { defineConfig, devices } from "@playwright/test" import * as dotenv from "dotenv" -dotenv.config({ path: "playwright/playwright.env" }) +dotenv.config({ path: __dirname + "/playwright/playwright.env" }) /** * See https://playwright.dev/docs/test-configuration. @@ -17,19 +17,28 @@ export default defineConfig({ workers: 1, reporter: process.env.CI ? [["github"], ["html"], ["list"]] : "html", - globalSetup: "./playwright/setup/global_setup.ts", - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { trace: process.env.CI ? "retain-on-failure" : "on", - baseURL: "http://localhost:19006", }, /* Configure projects for major browsers */ projects: [ { - name: "Mobile Chrome", - use: { ...devices["Pixel 5"] }, + name: "mobile-chrome", + testMatch: "mobile/**/*.spec.ts", + use: { + ...devices["Pixel 5"], + baseURL: "http://localhost:19006", + }, + }, + { + name: "chromium", + testMatch: "desktop/**/*.spec.ts", + use: { + ...devices["Desktop Chrome"], + baseURL: "http://localhost:4455", + }, }, ], @@ -42,7 +51,6 @@ export default defineConfig({ ].join(" && "), cwd: "../..", url: "http://localhost:4433/health/ready", - reuseExistingServer: false, env: { DSN: dbToDsn(), COURIER_SMTP_CONNECTION_URI: diff --git a/test/e2e/playwright/actions/login.ts b/test/e2e/playwright/actions/login.ts new file mode 100644 index 000000000000..806f16466dc3 --- /dev/null +++ b/test/e2e/playwright/actions/login.ts @@ -0,0 +1,47 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { APIRequestContext } from "@playwright/test" +import { findCsrfToken } from "../lib/helper" +import { LoginFlow, Session } from "@ory/kratos-client" +import { expectJSONResponse } from "../lib/request" +import { expect } from "../fixtures" + +export async function loginWithPassword( + user: { password: string; traits: { email: string } }, + r: APIRequestContext, + baseUrl: string, +): Promise { + const { ui } = await expectJSONResponse( + await r.get(baseUrl + "/self-service/login/browser", { + headers: { + Accept: "application/json", + }, + }), + { + message: "Initializing login flow failed", + }, + ) + + const res = await r.post(ui.action, { + headers: { + Accept: "application/json", + }, + data: { + identifier: user.traits.email, + password: user.password, + method: "password", + csrf_token: findCsrfToken(ui), + }, + }) + const { session } = await expectJSONResponse<{ session: Session }>(res) + expect(session?.identity?.traits.email).toEqual(user.traits.email) + expect( + res.headersArray().find( + ({ name, value }) => + name.toLowerCase() === "set-cookie" && + (value.indexOf("ory_session_") > -1 || // Ory Network + value.indexOf("ory_kratos_session") > -1), // Locally hosted + ), + ).toBeDefined() +} diff --git a/test/e2e/playwright/actions/mail.ts b/test/e2e/playwright/actions/mail.ts index 871608bc204d..172c6d8ab849 100644 --- a/test/e2e/playwright/actions/mail.ts +++ b/test/e2e/playwright/actions/mail.ts @@ -8,14 +8,29 @@ const mh = mailhog({ basePath: "http://localhost:8025/api", }) -export function search(...props: Parameters) { +type searchProps = { + query: string + kind: "to" | "from" | "containing" + /** + * + * @param message an email message + * @returns decide whether to include the message in the result + */ + filter?: (message: mailhog.Message) => boolean +} + +export function search({ query, kind, filter }: searchProps) { return retry( async () => { - const res = await mh.search(...props) + const res = await mh.search(query, kind) if (res.total === 0) { throw new Error("no emails found") } - return res.items + const result = filter ? res.items.filter(filter) : res.items + if (result.length === 0) { + throw new Error("no emails found") + } + return result }, { retries: 3, diff --git a/test/e2e/playwright/actions/session.ts b/test/e2e/playwright/actions/session.ts new file mode 100644 index 000000000000..5d77b6de7b59 --- /dev/null +++ b/test/e2e/playwright/actions/session.ts @@ -0,0 +1,38 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { APIRequestContext, expect } from "@playwright/test" +import { Session } from "@ory/kratos-client" + +export async function hasSession( + r: APIRequestContext, + kratosPublicURL: string, +): Promise { + const resp = await r.get(kratosPublicURL + "/sessions/whoami", { + failOnStatusCode: true, + }) + const session = await resp.json() + expect(session).toBeDefined() + expect(session.active).toBe(true) +} + +export async function getSession( + r: APIRequestContext, + kratosPublicURL: string, +): Promise { + const resp = await r.get(kratosPublicURL + "/sessions/whoami", { + failOnStatusCode: true, + }) + return resp.json() +} + +export async function hasNoSession( + r: APIRequestContext, + kratosPublicURL: string, +): Promise { + const resp = await r.get(kratosPublicURL + "/sessions/whoami", { + failOnStatusCode: false, + }) + expect(resp.status()).toBe(401) + return resp.json() +} diff --git a/test/e2e/playwright/fixtures/index.ts b/test/e2e/playwright/fixtures/index.ts index 3b1264e84c0a..b915dd4d937f 100644 --- a/test/e2e/playwright/fixtures/index.ts +++ b/test/e2e/playwright/fixtures/index.ts @@ -1,13 +1,24 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 +import { faker } from "@faker-js/faker" import { Identity } from "@ory/kratos-client" -import { test as base, expect } from "@playwright/test" -import { OryKratosConfiguration } from "../../cypress/support/config" +import { + CDPSession, + test as base, + expect as baseExpect, + APIRequestContext, + Page, +} from "@playwright/test" +import { writeFile } from "fs/promises" import { merge } from "lodash" +import { OryKratosConfiguration } from "../../shared/config" import { default_config } from "../setup/default_config" -import { writeFile } from "fs/promises" -import { faker } from "@faker-js/faker" +import { APIResponse } from "playwright-core" +import { SessionWithResponse } from "../types" +import { retryOptions } from "../lib/request" +import promiseRetry from "promise-retry" +import { Protocol } from "playwright-core/types/protocol" // from https://stackoverflow.com/questions/61132262/typescript-deep-partial type DeepPartial = T extends object @@ -17,12 +28,23 @@ type DeepPartial = T extends object : T type TestFixtures = { - identity: Identity + identity: { oryIdentity: Identity; email: string; password: string } configOverride: DeepPartial - config: void + config: OryKratosConfiguration + virtualAuthenticatorOptions: Partial + pageCDPSession: CDPSession + virtualAuthenticator: Protocol.WebAuthn.addVirtualAuthenticatorReturnValue } -type WorkerFixtures = {} +type WorkerFixtures = { + kratosAdminURL: string + kratosPublicURL: string + mode: + | "reconfigure_kratos" + | "reconfigure_ory_network_project" + | "existing_kratos" + | "existing_ory_network_project" +} export const test = base.extend({ configOverride: {}, @@ -34,9 +56,11 @@ export const test = base.extend({ const configRevision = await resp.body() + const fileDirectory = __dirname + "/../.." + await writeFile( - "playwright/kratos.config.json", - JSON.stringify(configToWrite), + fileDirectory + "/playwright/kratos.config.json", + JSON.stringify(configToWrite, null, 2), ) await expect(async () => { const resp = await request.get("http://localhost:4434/health/config") @@ -44,21 +68,166 @@ export const test = base.extend({ expect(updatedRevision).not.toBe(configRevision) }).toPass() - await use() + await use(configToWrite) }, { auto: true }, ], - identity: async ({ request }, use) => { + virtualAuthenticatorOptions: undefined, + pageCDPSession: async ({ page }, use) => { + const cdpSession = await page.context().newCDPSession(page) + await use(cdpSession) + await cdpSession.detach() + }, + virtualAuthenticator: async ( + { pageCDPSession, virtualAuthenticatorOptions }, + use, + ) => { + await pageCDPSession.send("WebAuthn.enable") + const { authenticatorId } = await pageCDPSession.send( + "WebAuthn.addVirtualAuthenticator", + { + options: { + protocol: "ctap2", + transport: "internal", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, + ...virtualAuthenticatorOptions, + }, + }, + ) + await use({ authenticatorId }) + await pageCDPSession.send("WebAuthn.removeVirtualAuthenticator", { + authenticatorId, + }) + + await pageCDPSession.send("WebAuthn.disable") + }, + identity: async ({ request }, use, i) => { + const email = faker.internet.email({ provider: "ory.sh" }) + const password = faker.internet.password() const resp = await request.post("http://localhost:4434/admin/identities", { data: { schema_id: "email", traits: { - email: faker.internet.email(undefined, undefined, "ory.sh"), + email, website: faker.internet.url(), }, + + credentials: { + password: { + config: { + password, + }, + }, + }, }, }) + const oryIdentity = await resp.json() + i.attach("identity", { + body: JSON.stringify(oryIdentity, null, 2), + contentType: "application/json", + }) expect(resp.status()).toBe(201) - await use(await resp.json()) + await use({ + oryIdentity, + email, + password, + }) }, + kratosAdminURL: ["http://localhost:4434", { option: true, scope: "worker" }], + kratosPublicURL: ["http://localhost:4433", { option: true, scope: "worker" }], +}) + +export const expect = baseExpect.extend({ + toHaveSession, + toMatchResponseData, }) + +async function toHaveSession( + requestOrPage: APIRequestContext | Page, + baseUrl: string, +) { + let r: APIRequestContext + if ("request" in requestOrPage) { + r = requestOrPage.request + } else { + r = requestOrPage + } + let pass = true + + let responseData: string + let response: APIResponse = null + try { + const result = await promiseRetry( + () => + r + .get(baseUrl + "/sessions/whoami", { + failOnStatusCode: false, + }) + .then( + async (res: APIResponse): Promise => { + return { + session: await res.json(), + response: res, + } + }, + ), + retryOptions, + ) + pass = !!result.session.active + responseData = await result.response.text() + response = result.response + } catch (e) { + pass = false + responseData = JSON.stringify(e.message, undefined, 2) + } + + const message = () => + this.utils.matcherHint("toHaveSession", undefined, undefined, { + isNot: this.isNot, + }) + + `\n + \n + Expected: ${this.isNot ? "not" : ""} to have session\n + Session data received: ${responseData}\n + Headers: ${JSON.stringify(response?.headers(), null, 2)}\n + ` + + return { + message, + pass, + name: "toHaveSession", + } +} + +async function toMatchResponseData( + res: APIResponse, + options: { + statusCode?: number + failureHint?: string + }, +) { + const body = await res.text() + const statusCode = options.statusCode ?? 200 + const failureHint = options.failureHint ?? "" + const message = () => + this.utils.matcherHint("toMatch", undefined, undefined, { + isNot: this.isNot, + }) + + `\n + ${failureHint} + \n + Expected: ${this.isNot ? "not" : ""} to match\n + Status Code: ${statusCode}\n + Body: ${body}\n + Headers: ${JSON.stringify(res.headers(), null, 2)}\n + URL: ${JSON.stringify(res.url(), null, 2)}\n + ` + + return { + message, + pass: res.status() === statusCode, + name: "toMatch", + } +} diff --git a/test/e2e/playwright/kratos.base-config.json b/test/e2e/playwright/kratos.base-config.json index 83b24587bd61..e9abcfd7f4f0 100644 --- a/test/e2e/playwright/kratos.base-config.json +++ b/test/e2e/playwright/kratos.base-config.json @@ -4,6 +4,10 @@ { "id": "default", "url": "file://test/e2e/profiles/oidc/identity.traits.schema.json" + }, + { + "id": "email", + "url": "file://test/e2e/profiles/email/identity.traits.schema.json" } ] }, diff --git a/test/e2e/playwright/lib/helper.ts b/test/e2e/playwright/lib/helper.ts index 929b39b74573..da5fb76c13ba 100644 --- a/test/e2e/playwright/lib/helper.ts +++ b/test/e2e/playwright/lib/helper.ts @@ -2,6 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 import { Message } from "mailhog" +import { + UiContainer, + UiNodeAttributes, + UiNodeInputAttributes, +} from "@ory/kratos-client" +import { expect } from "../fixtures" +import { LoginFlowStyle, OryKratosConfiguration } from "../../shared/config" export const codeRegex = /(\d{6})/ @@ -18,3 +25,50 @@ export function extractCode(mail: Message) { } return null } + +export function findCsrfToken(ui: UiContainer) { + const csrf = ui.nodes + .filter((node) => isUiNodeInputAttributes(node.attributes)) + // Since we filter all non-input attributes, the following as is ok: + .map( + (node): UiNodeInputAttributes => node.attributes as UiNodeInputAttributes, + ) + .find(({ name }) => name === "csrf_token")?.value + expect(csrf).toBeDefined() + return csrf +} + +export function isUiNodeInputAttributes( + attrs: UiNodeAttributes, +): attrs is UiNodeInputAttributes & { + node_type: "input" +} { + return attrs.node_type === "input" +} + +export const toConfig = ({ + style = "identifier_first", + mitigateEnumeration = false, + selfservice, +}: { + style?: LoginFlowStyle + mitigateEnumeration?: boolean + selfservice?: Partial +}) => ({ + selfservice: { + default_browser_return_url: "http://localhost:4455/welcome", + ...selfservice, + flows: { + login: { + ...selfservice?.flows?.login, + style, + }, + ...selfservice?.flows, + }, + }, + security: { + account_enumeration: { + mitigate: mitigateEnumeration, + }, + }, +}) diff --git a/test/e2e/playwright/lib/request.ts b/test/e2e/playwright/lib/request.ts new file mode 100644 index 000000000000..5732e5a34aca --- /dev/null +++ b/test/e2e/playwright/lib/request.ts @@ -0,0 +1,34 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { APIResponse } from "playwright-core" +import { expect } from "../fixtures" +import { OperationOptions } from "retry" + +export type RetryOptions = OperationOptions + +export const retryOptions: RetryOptions = { + retries: 20, + factor: 1, + maxTimeout: 500, + minTimeout: 250, + randomize: false, +} + +export async function expectJSONResponse( + res: APIResponse, + { statusCode = 200, message }: { statusCode?: number; message?: string } = {}, +): Promise { + await expect(res).toMatchResponseData({ + statusCode, + failureHint: message, + }) + try { + return (await res.json()) as T + } catch (e) { + const body = await res.text() + throw Error( + `Expected to be able to parse body as json: ${e} (body: ${body})`, + ) + } +} diff --git a/test/e2e/playwright/models/elements/login.ts b/test/e2e/playwright/models/elements/login.ts new file mode 100644 index 000000000000..a6ea12629db5 --- /dev/null +++ b/test/e2e/playwright/models/elements/login.ts @@ -0,0 +1,249 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { expect, Locator, Page } from "@playwright/test" +import { createInputLocator, InputLocator } from "../../selectors/input" +import { URLSearchParams } from "node:url" +import { OryKratosConfiguration } from "../../../shared/config" + +enum LoginStyle { + IdentifierFirst = "identifier_first", + Unified = "unified", +} + +type SubmitOptions = { + submitWithKeyboard?: boolean + waitForURL?: string | RegExp +} + +export class LoginPage { + public submitPassword: Locator + public github: Locator + public google: Locator + public signup: Locator + + public identifier: InputLocator + public password: InputLocator + public totpInput: InputLocator + public totpSubmit: Locator + public lookupInput: InputLocator + public lookupSubmit: Locator + public codeSubmit = this.page.locator('button[type="submit"][value="code"]') + public codeInput = createInputLocator(this.page, "code") + + public alert: Locator + + constructor(readonly page: Page, readonly config: OryKratosConfiguration) { + this.identifier = createInputLocator(page, "identifier") + this.password = createInputLocator(page, "password") + this.totpInput = createInputLocator(page, "totp_code") + this.lookupInput = createInputLocator(page, "lookup_secret") + + this.submitPassword = page.locator( + '[type="submit"][name="method"][value="password"]', + ) + + this.github = page.locator('[name="provider"][value="github"]') + this.google = page.locator('[name="provider"][value="google"]') + + this.totpSubmit = page.locator('[name="method"][value="totp"]') + this.lookupSubmit = page.locator('[name="method"][value="lookup_secret"]') + + this.signup = page.locator('[data-testid="signup-link"]') + + // this.submitHydra = page.locator('[name="provider"][value="hydra"]') + // this.forgotPasswordLink = page.locator( + // "[data-testid='forgot-password-link']", + // ) + // this.logoutLink = page.locator("[data-testid='logout-link']") + } + + async submitIdentifierFirst(identifier: string) { + await this.inputField("identifier").fill(identifier) + await this.submit("identifier_first", { + waitForURL: new RegExp(this.config.selfservice.flows.login.ui_url), + }) + } + + async loginWithPassword( + identifier: string, + password: string, + opts?: SubmitOptions, + ) { + switch (this.config.selfservice.flows.login.style) { + case LoginStyle.IdentifierFirst: + await this.submitIdentifierFirst(identifier) + break + case LoginStyle.Unified: + await this.inputField("identifier").fill(identifier) + break + } + + await this.inputField("password").fill(password) + await this.submit("password", opts) + } + + async triggerLoginWithCode(identifier: string, opts?: SubmitOptions) { + switch (this.config.selfservice.flows.login.style) { + case LoginStyle.IdentifierFirst: + await this.submitIdentifierFirst(identifier) + break + case LoginStyle.Unified: + await this.inputField("identifier").fill(identifier) + break + } + + await this.codeSubmit.click() + } + + async open({ + aal, + refresh, + }: { + aal?: string + refresh?: boolean + } = {}) { + const p = new URLSearchParams() + if (refresh) { + p.append("refresh", "true") + } + + if (aal) { + p.append("aal", aal) + } + + await Promise.all([ + this.page.goto( + this.config.selfservice.flows.login.ui_url + "?" + p.toString(), + ), + this.isReady(), + this.page.waitForURL((url) => + url.toString().includes(this.config.selfservice.flows.login.ui_url), + ), + ]) + await this.isReady() + } + + async isReady() { + await expect(this.inputField("csrf_token").nth(0)).toBeHidden() + } + + submitMethod(method: string) { + switch (method) { + case "google": + case "github": + case "hydra": + return this.page.locator(`[name="provider"][value="${method}"]`) + } + return this.page.locator(`[name="method"][value="${method}"]`) + } + + inputField(name: string) { + return this.page.locator(`input[name=${name}]`) + } + + async submit(method: string, opts?: SubmitOptions) { + const waitFor = [ + opts?.waitForURL + ? this.page.waitForURL(opts.waitForURL) + : Promise.resolve(), + ] + + if (opts?.submitWithKeyboard) { + waitFor.push(this.page.keyboard.press("Enter")) + } else { + waitFor.push(this.submitMethod(method).click()) + } + + await Promise.all(waitFor) + } + + // + // async submitPasswordForm( + // id: string, + // password: string, + // expectURL: string | RegExp, + // options: { + // submitWithKeyboard?: boolean + // style?: LoginStyle + // } = { + // submitWithKeyboard: false, + // style: LoginStyle.OneStep, + // }, + // ) { + // await this.isReady() + // await this.inputField("identifier").fill(id) + // + // if (options.style === LoginStyle.IdentifierFirst) { + // await this.submitMethod("identifier_first").click() + // await this.inputField("password").fill(password) + // } else { + // await this.inputField("password").fill(password) + // } + // + // const nav = this.page.waitForURL(expectURL) + // + // if (submitWithKeyboard) { + // await this.page.keyboard.press("Enter") + // } else { + // await this.submitPassword.click() + // } + // + // await nav + // } + // + // readonly baseURL: string + // readonly submitHydra: Locator + // readonly forgotPasswordLink: Locator + // readonly logoutLink: Locator + // + // async goto(returnTo?: string, refresh?: boolean) { + // const u = new URL(routes.hosted.login(this.baseURL)) + // if (returnTo) { + // u.searchParams.append("return_to", returnTo) + // } + // if (refresh) { + // u.searchParams.append("refresh", refresh.toString()) + // } + // await this.page.goto(u.toString()) + // await this.isReady() + // } + // + // async loginWithHydra(email: string, password: string) { + // await this.submitHydra.click() + // await this.page.waitForURL(new RegExp(OIDC_PROVIDER)) + // + // await this.page.locator("input[name=email]").fill(email) + // await this.page.locator("input[name=password]").fill(password) + // + // await this.page.locator("input[name=submit][id=accept]").click() + // } + // + // async loginWithOIDC(email = generateEmail(), password = generatePassword()) { + // await this.page.fill('[name="email"]', email) + // await this.page.fill('[name="password"]', password) + // await this.page.click("#accept") + // } + // + // async loginAndAcceptConsent( + // email = generateEmail(), + // password = generatePassword(), + // {rememberConsent = true, rememberLogin = false} = {}, + // ) { + // await this.page.fill('[name="email"]', email) + // await this.page.fill('[name="password"]', password) + // rememberLogin && (await this.page.check('[name="remember"]')) + // await this.page.click("#accept") + // + // await this.page.click("#offline") + // await this.page.click("#openid") + // rememberConsent && (await this.page.check("[name=remember]")) + // await this.page.click("#accept") + // + // return email + // } + // + // async expectAlert(id: string) { + // await this.page.getByTestId(`ui/message/${id}`).waitFor() + // } +} diff --git a/test/e2e/playwright/selectors/input.ts b/test/e2e/playwright/selectors/input.ts new file mode 100644 index 000000000000..c4f1f35d0271 --- /dev/null +++ b/test/e2e/playwright/selectors/input.ts @@ -0,0 +1,19 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { Locator, Page } from "@playwright/test" + +export interface InputLocator { + input: Locator + message: Locator + label: Locator +} + +export const createInputLocator = (page: Page, field: string): InputLocator => { + const prefix = `[data-testid="node/input/${field}"]` + return { + input: page.locator(`${prefix} input`), + label: page.locator(`${prefix} label`), + message: page.locator(`${prefix} p`), + } +} diff --git a/test/e2e/playwright/setup/default_config.ts b/test/e2e/playwright/setup/default_config.ts index b9249917b039..5e4f5d1e2e73 100644 --- a/test/e2e/playwright/setup/default_config.ts +++ b/test/e2e/playwright/setup/default_config.ts @@ -1,7 +1,7 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import { OryKratosConfiguration } from "../../cypress/support/config" +import { OryKratosConfiguration } from "../../shared/config" export const default_config: OryKratosConfiguration = { dsn: "", @@ -11,6 +11,10 @@ export const default_config: OryKratosConfiguration = { id: "default", url: "file://test/e2e/profiles/oidc/identity.traits.schema.json", }, + { + id: "email", + url: "file://test/e2e/profiles/email/identity.traits.schema.json", + }, ], }, serve: { @@ -40,7 +44,7 @@ export const default_config: OryKratosConfiguration = { cipher: ["secret-thirty-two-character-long"], }, selfservice: { - default_browser_return_url: "http://localhost:4455/", + default_browser_return_url: "http://localhost:4455/welcome", allowed_return_urls: [ "http://localhost:4455", "http://localhost:19006", diff --git a/test/e2e/playwright/setup/global_setup.ts b/test/e2e/playwright/setup/global_setup.ts deleted file mode 100644 index 92a42432fc96..000000000000 --- a/test/e2e/playwright/setup/global_setup.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -import fs from "fs" -import { default_config } from "./default_config" - -export default async function globalSetup() { - await fs.promises.writeFile( - "playwright/kratos.config.json", - JSON.stringify(default_config), - ) -} diff --git a/test/e2e/playwright/tests/desktop/identifier_first/code.login.spec.ts b/test/e2e/playwright/tests/desktop/identifier_first/code.login.spec.ts new file mode 100644 index 000000000000..5cfe7c8f7045 --- /dev/null +++ b/test/e2e/playwright/tests/desktop/identifier_first/code.login.spec.ts @@ -0,0 +1,227 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { expect } from "@playwright/test" +import { search } from "../../../actions/mail" +import { getSession, hasNoSession, hasSession } from "../../../actions/session" +import { test } from "../../../fixtures" +import { extractCode, toConfig } from "../../../lib/helper" +import { LoginPage } from "../../../models/elements/login" + +test.describe("account enumeration protection off", () => { + test.use({ + configOverride: toConfig({ + style: "identifier_first", + mitigateEnumeration: false, + selfservice: { + methods: { + code: { + passwordless_enabled: true, + }, + }, + }, + }), + }) + + test("login fails because user does not exist", async ({ page, config }) => { + const login = new LoginPage(page, config) + await login.open() + + await login.submitIdentifierFirst("i@donot.exist") + + await expect( + page.locator('[data-testid="ui/message/4000037"]'), + "expect account not exist message to be shown", + ).toBeVisible() + }) + + test("login with wrong code fails", async ({ + page, + identity, + kratosPublicURL, + config, + }) => { + const login = new LoginPage(page, config) + await login.open() + + await login.triggerLoginWithCode(identity.email) + + await login.codeInput.input.fill("123123") + + await login.codeSubmit.getByText("Continue").click() + + await hasNoSession(page.request, kratosPublicURL) + await expect( + page.locator('[data-testid="ui/message/4010008"]'), + "expect to be shown a wrong code error", + ).toBeVisible() + }) + + test("login succeeds", async ({ + page, + identity, + config, + kratosPublicURL, + }) => { + const login = new LoginPage(page, config) + await login.open() + + await login.triggerLoginWithCode(identity.email) + + const mails = await search({ query: identity.email, kind: "to" }) + expect(mails).toHaveLength(1) + + const code = extractCode(mails[0]) + + await login.codeInput.input.fill(code) + + await login.codeSubmit.getByText("Continue").click() + + await hasSession(page.request, kratosPublicURL) + }) +}) + +test.describe("account enumeration protection on", () => { + test.use({ + configOverride: toConfig({ + style: "identifier_first", + mitigateEnumeration: true, + selfservice: { + methods: { + password: { + enabled: false, + }, + code: { + passwordless_enabled: true, + }, + }, + }, + }), + }) + + test("login fails because user does not exist", async ({ page, config }) => { + const login = new LoginPage(page, config) + await login.open() + + await login.submitIdentifierFirst("i@donot.exist") + + await expect( + page.locator('button[name="method"][value="code"]'), + "expect to show the code form", + ).toBeVisible() + }) + + test("login with wrong code fails", async ({ + page, + identity, + kratosPublicURL, + config, + }) => { + const login = new LoginPage(page, config) + await login.open() + + await login.triggerLoginWithCode(identity.email) + + await login.codeInput.input.fill("123123") + + await login.codeSubmit.getByText("Continue").click() + + await hasNoSession(page.request, kratosPublicURL) + await expect( + page.locator('[data-testid="ui/message/4010008"]'), + "expect to be shown a wrong code error", + ).toBeVisible() + }) + + test("login succeeds", async ({ + page, + identity, + config, + kratosPublicURL, + }) => { + const login = new LoginPage(page, config) + await login.open() + + await login.triggerLoginWithCode(identity.email) + + const mails = await search({ query: identity.email, kind: "to" }) + expect(mails).toHaveLength(1) + + const code = extractCode(mails[0]) + + await login.codeInput.input.fill(code) + + await login.codeSubmit.getByText("Continue").click() + + await hasSession(page.request, kratosPublicURL) + }) +}) + +test.describe(() => { + test.use({ + configOverride: toConfig({ + style: "identifier_first", + mitigateEnumeration: false, + selfservice: { + methods: { + code: { + passwordless_enabled: true, + }, + }, + }, + }), + }) + test("refresh", async ({ page, identity, config, kratosPublicURL }) => { + const login = new LoginPage(page, config) + + const [initialSession, initialCode] = + await test.step("initial login", async () => { + await login.open() + await login.triggerLoginWithCode(identity.email) + + const mails = await search({ query: identity.email, kind: "to" }) + expect(mails).toHaveLength(1) + + const code = extractCode(mails[0]) + + await login.codeInput.input.fill(code) + + await login.codeSubmit.getByText("Continue").click() + + const session = await getSession(page.request, kratosPublicURL) + expect(session).toBeDefined() + expect(session.active).toBe(true) + return [session, code] + }) + + await login.open({ + refresh: true, + }) + await login.inputField("identifier").fill(identity.email) + await login.submit("code") + + const mails = await search({ + query: identity.email, + kind: "to", + filter: (m) => !m.html.includes(initialCode), + }) + expect(mails).toHaveLength(1) + + const code = extractCode(mails[0]) + + await login.codeInput.input.fill(code) + + await login.codeSubmit.getByText("Continue").click() + await page.waitForURL( + new RegExp(config.selfservice.default_browser_return_url), + ) + + const newSession = await getSession(page.request, kratosPublicURL) + expect(newSession).toBeDefined() + expect(newSession.active).toBe(true) + + const initDate = Date.parse(initialSession.authenticated_at) + const newDate = Date.parse(newSession.authenticated_at) + expect(newDate).toBeGreaterThanOrEqual(initDate) + }) +}) diff --git a/test/e2e/playwright/tests/desktop/identifier_first/oidc.login.spec.ts b/test/e2e/playwright/tests/desktop/identifier_first/oidc.login.spec.ts new file mode 100644 index 000000000000..5568b88a1aab --- /dev/null +++ b/test/e2e/playwright/tests/desktop/identifier_first/oidc.login.spec.ts @@ -0,0 +1,171 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { faker } from "@faker-js/faker" +import { expect, Page } from "@playwright/test" +import { getSession, hasSession } from "../../../actions/session" +import { test } from "../../../fixtures" +import { toConfig } from "../../../lib/helper" +import { LoginPage } from "../../../models/elements/login" +import { OryKratosConfiguration } from "../../../../shared/config" + +async function loginHydra(page: Page) { + return test.step("login with hydra", async () => { + await page + .locator("input[name=username]") + .fill(faker.internet.email({ provider: "ory.sh" })) + await page.locator("button[name=action][value=accept]").click() + await page.locator("#offline").check() + await page.locator("#openid").check() + + await page.locator("input[name=website]").fill(faker.internet.url()) + + await page.locator("button[name=action][value=accept]").click() + }) +} + +async function registerWithHydra( + page: Page, + config: OryKratosConfiguration, + kratosPublicURL: string, +) { + return await test.step("register", async () => { + await page.goto("/registration") + + await page.locator(`button[name=provider][value=hydra]`).click() + + const email = faker.internet.email({ provider: "ory.sh" }) + await page.locator("input[name=username]").fill(email) + await page.locator("#remember").check() + await page.locator("button[name=action][value=accept]").click() + await page.locator("#offline").check() + await page.locator("#openid").check() + + await page.locator("input[name=website]").fill(faker.internet.url()) + + await page.locator("button[name=action][value=accept]").click() + await page.waitForURL( + new RegExp(config.selfservice.default_browser_return_url), + ) + await page.context().clearCookies({ + domain: new URL(kratosPublicURL).hostname, + }) + + await expect( + getSession(page.request, kratosPublicURL), + ).rejects.toThrowError() + return email + }) +} + +for (const mitigateEnumeration of [true, false]) { + test.describe(`account enumeration protection ${ + mitigateEnumeration ? "on" : "off" + }`, () => { + test.use({ + configOverride: toConfig({ + mitigateEnumeration, + selfservice: { + methods: { + password: { + enabled: true, + }, + }, + }, + }), + }) + + test("login", async ({ page, config, kratosPublicURL }) => { + const login = new LoginPage(page, config) + await login.open() + + await page.locator(`button[name=provider][value=hydra]`).click() + + await loginHydra(page) + + await page.waitForURL( + new RegExp(config.selfservice.default_browser_return_url), + ) + + await hasSession(page.request, kratosPublicURL) + }) + + test("oidc sign in on second step", async ({ + page, + config, + kratosPublicURL, + }) => { + const email = await registerWithHydra(page, config, kratosPublicURL) + + const login = new LoginPage(page, config) + await login.open() + + await login.submitIdentifierFirst(email) + + // If account enumeration is mitigated, we should see the password method, + // because the identity has not set up a password + await expect( + page.locator('button[name="method"][value="password"]'), + "hide the password method", + ).toBeVisible({ visible: mitigateEnumeration }) + + await page.locator(`button[name=provider][value=hydra]`).click() + + await loginHydra(page) + + await page.waitForURL( + new RegExp(config.selfservice.default_browser_return_url), + ) + + const session = await getSession(page.request, kratosPublicURL) + expect(session).toBeDefined() + expect(session.active).toBe(true) + }) + }) +} + +test("login with refresh", async ({ page, config, kratosPublicURL }) => { + await registerWithHydra(page, config, kratosPublicURL) + + const login = new LoginPage(page, config) + + const initialSession = await test.step("initial login", async () => { + await login.open() + await page.locator(`button[name=provider][value=hydra]`).click() + + await loginHydra(page) + + await page.waitForURL( + new RegExp(config.selfservice.default_browser_return_url), + ) + return await getSession(page.request, kratosPublicURL) + }) + + // This is required, because OIDC issues a new session on refresh (TODO), and MySQL does not store sub second timestamps, so we need to wait a bit + await page.waitForTimeout(1000) + await test.step("refresh login", async () => { + await login.open({ + refresh: true, + }) + + await expect( + page.locator('[data-testid="ui/message/1010003"]'), + "show the refresh message", + ).toBeVisible() + + await page.locator(`button[name=provider][value=hydra]`).click() + + await loginHydra(page) + + await page.waitForURL( + new RegExp(config.selfservice.default_browser_return_url), + ) + const newSession = await getSession(page.request, kratosPublicURL) + // expect(newSession.authentication_methods).toHaveLength( + // initialSession.authentication_methods.length + 1, + // ) + expect(newSession.authenticated_at).not.toBe( + initialSession.authenticated_at, + ) + }) +}) diff --git a/test/e2e/playwright/tests/desktop/identifier_first/passkeys.login.spec.ts b/test/e2e/playwright/tests/desktop/identifier_first/passkeys.login.spec.ts new file mode 100644 index 000000000000..ecd4fe0ceaa5 --- /dev/null +++ b/test/e2e/playwright/tests/desktop/identifier_first/passkeys.login.spec.ts @@ -0,0 +1,238 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { faker } from "@faker-js/faker" +import { CDPSession, expect, Page } from "@playwright/test" +import { OryKratosConfiguration } from "../../../../shared/config" +import { getSession } from "../../../actions/session" +import { test } from "../../../fixtures" +import { toConfig } from "../../../lib/helper" +import { LoginPage } from "../../../models/elements/login" + +async function toggleAutomaticPresenceSimulation( + cdpSession: CDPSession, + authenticatorId: string, + enabled: boolean, +) { + await cdpSession.send("WebAuthn.setAutomaticPresenceSimulation", { + authenticatorId, + enabled, + }) +} + +async function registerWithPasskey( + page: Page, + pageCDPSession: CDPSession, + config: OryKratosConfiguration, + authenticatorId: string, + simulatePresence: boolean, +) { + return await test.step("create webauthn identity", async () => { + await page.goto("/registration") + const identifier = faker.internet.email() + await page.locator(`input[name="traits.email"]`).fill(identifier) + await page + .locator(`input[name="traits.website"]`) + .fill(faker.internet.url()) + await page.locator("button[name=method][value=profile]").click() + + await toggleAutomaticPresenceSimulation( + pageCDPSession, + authenticatorId, + true, + ) + await page.locator("button[name=passkey_register_trigger]").click() + + await toggleAutomaticPresenceSimulation( + pageCDPSession, + authenticatorId, + simulatePresence, + ) + + await page.waitForURL( + new RegExp(config.selfservice.default_browser_return_url), + ) + return identifier + }) +} + +const passkeyConfig = { + methods: { + passkey: { + enabled: true, + config: { + rp: { + display_name: "ORY", + id: "localhost", + origins: ["http://localhost:4455"], + }, + }, + }, + }, +} + +for (const mitigateEnumeration of [true, false]) { + test.describe(`account enumeration protection ${ + mitigateEnumeration ? "on" : "off" + }`, () => { + test.use({ + configOverride: toConfig({ + mitigateEnumeration, + style: "identifier_first", + selfservice: passkeyConfig, + }), + }) + + for (const simulatePresence of [true, false]) { + test.describe(`${ + simulatePresence ? "with" : "without" + } automatic presence proof`, () => { + test.use({ + virtualAuthenticatorOptions: { + automaticPresenceSimulation: simulatePresence, + // hasResidentKey: simulatePresence, + }, + }) + test("login", async ({ + config, + page, + kratosPublicURL, + virtualAuthenticator, + pageCDPSession, + }) => { + const identifier = await registerWithPasskey( + page, + pageCDPSession, + config, + virtualAuthenticator.authenticatorId, + simulatePresence, + ) + await page.context().clearCookies({}) + + const login = new LoginPage(page, config) + await login.open() + + if (!simulatePresence) { + await login.submitIdentifierFirst(identifier) + + const passkeyLoginTrigger = page.locator( + "button[name=passkey_login_trigger]", + ) + await passkeyLoginTrigger.waitFor() + + await page.waitForLoadState("load") + + await toggleAutomaticPresenceSimulation( + pageCDPSession, + virtualAuthenticator.authenticatorId, + true, + ) + + await passkeyLoginTrigger.click() + + await toggleAutomaticPresenceSimulation( + pageCDPSession, + virtualAuthenticator.authenticatorId, + false, + ) + } + + await page.waitForURL( + new RegExp(config.selfservice.default_browser_return_url), + ) + + await expect( + getSession(page.request, kratosPublicURL), + ).resolves.toMatchObject({ + active: true, + identity: { + traits: { + email: identifier, + }, + }, + }) + }) + }) + } + }) +} + +test.describe("without automatic presence simulation", () => { + test.use({ + virtualAuthenticatorOptions: { + automaticPresenceSimulation: false, + }, + configOverride: toConfig({ + selfservice: passkeyConfig, + }), + }) + test("login with refresh", async ({ + page, + config, + kratosPublicURL, + pageCDPSession, + virtualAuthenticator, + }) => { + const identifier = await registerWithPasskey( + page, + pageCDPSession, + config, + virtualAuthenticator.authenticatorId, + true, + ) + + const login = new LoginPage(page, config) + // Due to resetting automatic presence simulating to "true" in the previous step, + // opening the login page automatically triggers the passkey login + await login.open() + + await page.waitForURL( + new RegExp(config.selfservice.default_browser_return_url), + ) + + await expect( + getSession(page.request, kratosPublicURL), + ).resolves.toMatchObject({ + active: true, + identity: { + traits: { + email: identifier, + }, + }, + }) + + await login.open({ + refresh: true, + }) + + await expect( + page.locator('[data-testid="ui/message/1010003"]'), + "show the refresh message", + ).toBeVisible() + + const initialSession = await getSession(page.request, kratosPublicURL) + + const passkeyLoginTrigger = page.locator( + "button[name=passkey_login_trigger]", + ) + await passkeyLoginTrigger.waitFor() + + await page.waitForLoadState("load") + + await toggleAutomaticPresenceSimulation( + pageCDPSession, + virtualAuthenticator.authenticatorId, + true, + ) + + await passkeyLoginTrigger.click() + await page.waitForURL( + new RegExp(config.selfservice.default_browser_return_url), + ) + const newSession = await getSession(page.request, kratosPublicURL) + + expect(newSession.authentication_methods).toHaveLength( + initialSession.authentication_methods.length + 1, + ) + }) +}) diff --git a/test/e2e/playwright/tests/desktop/identifier_first/password.login.spec.ts b/test/e2e/playwright/tests/desktop/identifier_first/password.login.spec.ts new file mode 100644 index 000000000000..fd6026cb39fb --- /dev/null +++ b/test/e2e/playwright/tests/desktop/identifier_first/password.login.spec.ts @@ -0,0 +1,236 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { expect } from "@playwright/test" +import { loginWithPassword } from "../../../actions/login" +import { getSession, hasNoSession, hasSession } from "../../../actions/session" +import { test } from "../../../fixtures" +import { toConfig } from "../../../lib/helper" +import { LoginPage } from "../../../models/elements/login" + +// These can run in parallel because they use the same config. +test.describe("account enumeration protection off", () => { + test.use({ + configOverride: toConfig({ + style: "identifier_first", + mitigateEnumeration: false, + selfservice: { + methods: { + password: { + enabled: true, + }, + code: { + passwordless_enabled: false, + }, + }, + }, + }), + }) + + test("login fails because user does not exist", async ({ page, config }) => { + const login = new LoginPage(page, config) + await login.open() + + await login.submitIdentifierFirst("i@donot.exist") + + await expect( + page.locator('[data-testid="ui/message/4000037"]'), + "expect account not exist message to be shown", + ).toBeVisible() + }) + + test("login with wrong password fails", async ({ + page, + identity, + kratosPublicURL, + config, + }) => { + const login = new LoginPage(page, config) + await login.open() + + await login.loginWithPassword(identity.email, "wrong-password") + await login.isReady() + + await hasNoSession(page.request, kratosPublicURL) + await expect( + page.locator('[data-testid="ui/message/4000006"]'), + "expect to be shown a credentials do not exist error", + ).toBeVisible() + }) + + test("login succeeds", async ({ + page, + identity, + config, + kratosPublicURL, + }) => { + const login = new LoginPage(page, config) + await login.open() + + await login.inputField("identifier").fill(identity.email) + await login.submit("identifier_first", { + waitForURL: new RegExp(config.selfservice.flows.login.ui_url), + }) + + await login.inputField("password").fill(identity.password) + await login.submit("password", { + waitForURL: new RegExp(config.selfservice.default_browser_return_url), + }) + + await hasSession(page.request, kratosPublicURL) + }) + + test("login with refresh", async ({ + page, + config, + identity, + kratosPublicURL, + }) => { + await loginWithPassword( + { + password: identity.password, + traits: { + email: identity.email, + }, + }, + page.request, + kratosPublicURL, + ) + + const login = new LoginPage(page, config) + await login.open({ + refresh: true, + }) + + await expect( + page.locator('[data-testid="ui/message/1010003"]'), + "show the refresh message", + ).toBeVisible() + + const initialSession = await getSession(page.request, kratosPublicURL) + await login.inputField("password").fill(identity.password) + await login.submit("password", { + waitForURL: new RegExp(config.selfservice.default_browser_return_url), + }) + + const newSession = await getSession(page.request, kratosPublicURL) + + expect(newSession.authentication_methods).toHaveLength( + initialSession.authentication_methods.length + 1, + ) + }) +}) + +test.describe("account enumeration protection on", () => { + test.use({ + configOverride: toConfig({ + style: "identifier_first", + mitigateEnumeration: true, + selfservice: { + methods: { + password: { + enabled: true, + }, + code: { + passwordless_enabled: false, + }, + }, + }, + }), + }) + + test("login fails because user does not exist", async ({ page, config }) => { + const login = new LoginPage(page, config) + await login.open() + + await login.submitIdentifierFirst("i@donot.exist") + + await expect( + page.locator('button[name="method"][value="password"]'), + "expect to show the password form", + ).toBeVisible() + }) + + test("login with wrong password fails", async ({ + page, + identity, + kratosPublicURL, + config, + }) => { + const login = new LoginPage(page, config) + await login.open() + + await login.loginWithPassword(identity.email, "wrong-password") + await login.isReady() + + await hasNoSession(page.request, kratosPublicURL) + await expect( + page.locator('[data-testid="ui/message/4000006"]'), + "expect to be shown a credentials do not exist error", + ).toBeVisible() + }) + + test("login succeeds", async ({ + page, + // projectFrontendClient, + identity, + config, + kratosPublicURL, + }) => { + const login = new LoginPage(page, config) + await login.open() + + await login.inputField("identifier").fill(identity.email) + await login.submit("identifier_first", { + waitForURL: new RegExp(config.selfservice.flows.login.ui_url), + }) + + await login.inputField("password").fill(identity.password) + await login.submit("password", { + waitForURL: new RegExp(config.selfservice.default_browser_return_url), + }) + + await hasSession(page.request, kratosPublicURL) + }) + + test("login with refresh", async ({ + page, + config, + identity, + kratosPublicURL, + }) => { + await loginWithPassword( + { + password: identity.password, + traits: { + email: identity.email, + }, + }, + page.request, + kratosPublicURL, + ) + + const login = new LoginPage(page, config) + await login.open({ + refresh: true, + }) + + await expect( + page.locator('[data-testid="ui/message/1010003"]'), + "show the refresh message", + ).toBeVisible() + + const initialSession = await getSession(page.request, kratosPublicURL) + + await login.inputField("password").fill(identity.password) + await login.submit("password", { + waitForURL: new RegExp(config.selfservice.default_browser_return_url), + }) + + const newSession = await getSession(page.request, kratosPublicURL) + + expect(newSession.authentication_methods).toHaveLength( + initialSession.authentication_methods.length + 1, + ) + }) +}) diff --git a/test/e2e/playwright/tests/app_login.spec.ts b/test/e2e/playwright/tests/mobile/app_login.spec.ts similarity index 97% rename from test/e2e/playwright/tests/app_login.spec.ts rename to test/e2e/playwright/tests/mobile/app_login.spec.ts index 600a49a48ce1..6dd6a93ba551 100644 --- a/test/e2e/playwright/tests/app_login.spec.ts +++ b/test/e2e/playwright/tests/mobile/app_login.spec.ts @@ -1,9 +1,8 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import { test, expect, Page } from "@playwright/test" - -test.describe.configure({ mode: "parallel" }) +import { expect, Page } from "@playwright/test" +import { test } from "../../fixtures" async function performOidcLogin(popup: Page, username: string) { await popup.waitForLoadState() diff --git a/test/e2e/playwright/tests/app_recovery.spec.ts b/test/e2e/playwright/tests/mobile/app_recovery.spec.ts similarity index 78% rename from test/e2e/playwright/tests/app_recovery.spec.ts rename to test/e2e/playwright/tests/mobile/app_recovery.spec.ts index 629abd3c05bc..c065e0965443 100644 --- a/test/e2e/playwright/tests/app_recovery.spec.ts +++ b/test/e2e/playwright/tests/mobile/app_recovery.spec.ts @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 import { expect } from "@playwright/test" -import { test } from "../fixtures" -import { search } from "../actions/mail" -import { extractCode } from "../lib/helper" +import { test } from "../../fixtures" +import { search } from "../../actions/mail" +import { extractCode } from "../../lib/helper" const schemaConfig = { default_schema_id: "email", @@ -31,11 +31,11 @@ test.describe("Recovery", () => { test("succeeds with a valid email address", async ({ page, identity }) => { await page.goto("/Recovery") - await page.getByTestId("email").fill(identity.traits.email) + await page.getByTestId("email").fill(identity.email) await page.getByTestId("submit-form").click() await expect(page.getByTestId("ui/message/1060003")).toBeVisible() - const mails = await search(identity.traits.email, "to") + const mails = await search({ query: identity.email, kind: "to" }) expect(mails).toHaveLength(1) const code = extractCode(mails[0]) @@ -43,13 +43,13 @@ test.describe("Recovery", () => { await test.step("enter wrong code", async () => { await page.getByTestId("code").fill(wrongCode) - await page.getByText("Submit").click() + await page.getByText("Continue").click() await expect(page.getByTestId("ui/message/4060006")).toBeVisible() }) await test.step("enter correct code", async () => { await page.getByTestId("code").fill(code) - await page.getByText("Submit").click() + await page.getByText("Continue").click() await page.waitForURL(/Settings/) await expect(page.getByTestId("ui/message/1060001").first()).toBeVisible() }) @@ -58,13 +58,13 @@ test.describe("Recovery", () => { test("wrong email address does not get sent", async ({ page, identity }) => { await page.goto("/Recovery") - const wrongEmailAddress = "wrong-" + identity.traits.email + const wrongEmailAddress = "wrong-" + identity.email await page.getByTestId("email").fill(wrongEmailAddress) await page.getByTestId("submit-form").click() await expect(page.getByTestId("ui/message/1060003")).toBeVisible() try { - await search(identity.traits.email, "to") + await search({ query: identity.email, kind: "to" }) expect(false).toBeTruthy() } catch (e) { // this is expected @@ -74,11 +74,11 @@ test.describe("Recovery", () => { test("fails with an invalid code", async ({ page, identity }) => { await page.goto("/Recovery") - await page.getByTestId("email").fill(identity.traits.email) + await page.getByTestId("email").fill(identity.email) await page.getByTestId("submit-form").click() await page.getByTestId("ui/message/1060003").isVisible() - const mails = await search(identity.traits.email, "to") + const mails = await search({ query: identity.email, kind: "to" }) expect(mails).toHaveLength(1) const code = extractCode(mails[0]) @@ -87,14 +87,14 @@ test.describe("Recovery", () => { await test.step("enter wrong repeatedly", async () => { for (let i = 0; i < 10; i++) { await page.getByTestId("code").fill(wrongCode) - await page.getByText("Submit", { exact: true }).click() + await page.getByText("Continue", { exact: true }).click() await expect(page.getByTestId("ui/message/4060006")).toBeVisible() } }) await test.step("enter correct code fails", async () => { await page.getByTestId("code").fill(code) - await page.getByText("Submit", { exact: true }).click() + await page.getByText("Continue", { exact: true }).click() await expect(page.getByTestId("ui/message/4060006")).toBeVisible() }) }) @@ -123,17 +123,17 @@ test.describe("Recovery", () => { test("fails with an expired code", async ({ page, identity }) => { await page.goto("/Recovery") - await page.getByTestId("email").fill(identity.traits.email) + await page.getByTestId("email").fill(identity.email) await page.getByTestId("submit-form").click() await page.getByTestId("ui/message/1060003").isVisible() - const mails = await search(identity.traits.email, "to") + const mails = await search({ query: identity.email, kind: "to" }) expect(mails).toHaveLength(1) const code = extractCode(mails[0]) await page.getByTestId("code").fill(code) - await page.getByText("Submit", { exact: true }).click() + await page.getByText("Continue", { exact: true }).click() await expect(page.getByTestId("email")).toBeVisible() }) }) diff --git a/test/e2e/playwright/types/index.ts b/test/e2e/playwright/types/index.ts new file mode 100644 index 000000000000..ddb3431774c0 --- /dev/null +++ b/test/e2e/playwright/types/index.ts @@ -0,0 +1,10 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { APIResponse } from "playwright-core" +import { Session } from "@ory/kratos-client" + +export type SessionWithResponse = { + session: Session + response: APIResponse +} diff --git a/test/e2e/profiles/code/.kratos.yml b/test/e2e/profiles/code/.kratos.yml index 58f050aa83e9..a98e4979e730 100644 --- a/test/e2e/profiles/code/.kratos.yml +++ b/test/e2e/profiles/code/.kratos.yml @@ -9,10 +9,12 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true ui_url: http://localhost:4455/registration after: code: hooks: + - hook: show_verification_ui - hook: session login: @@ -36,7 +38,6 @@ selfservice: enabled: true code: passwordless_enabled: true - passwordless_login_fallback_enabled: false enabled: true config: lifespan: 1h diff --git a/test/e2e/profiles/email/.kratos.yml b/test/e2e/profiles/email/.kratos.yml index 2176f282084a..b1d62a3e25c4 100644 --- a/test/e2e/profiles/email/.kratos.yml +++ b/test/e2e/profiles/email/.kratos.yml @@ -9,6 +9,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true ui_url: http://localhost:4455/registration after: password: diff --git a/test/e2e/profiles/email/identity.traits.schema.json b/test/e2e/profiles/email/identity.traits.schema.json index 59a799a43d20..e5d40a431bbd 100644 --- a/test/e2e/profiles/email/identity.traits.schema.json +++ b/test/e2e/profiles/email/identity.traits.schema.json @@ -17,6 +17,10 @@ "password": { "identifier": true }, + "code": { + "identifier": true, + "via": "email" + }, "webauthn": { "identifier": true } diff --git a/test/e2e/profiles/mfa/.kratos.yml b/test/e2e/profiles/mfa/.kratos.yml index 1ce3f4ec8724..081b3bb541b7 100644 --- a/test/e2e/profiles/mfa/.kratos.yml +++ b/test/e2e/profiles/mfa/.kratos.yml @@ -10,6 +10,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true ui_url: http://localhost:4455/registration after: password: @@ -26,6 +27,8 @@ selfservice: ui_url: http://localhost:4455/recovery methods: + code: + mfa_enabled: true totp: enabled: true config: @@ -43,7 +46,7 @@ selfservice: identity: schemas: - id: default - url: file://test/e2e/profiles/email/identity.traits.schema.json + url: file://test/e2e/profiles/mfa/identity.traits.schema.json session: whoami: diff --git a/test/e2e/profiles/mfa/identity.traits.schema.json b/test/e2e/profiles/mfa/identity.traits.schema.json index 87db142ee8d2..36aef3680f4d 100644 --- a/test/e2e/profiles/mfa/identity.traits.schema.json +++ b/test/e2e/profiles/mfa/identity.traits.schema.json @@ -19,6 +19,10 @@ }, "webauthn": { "identifier": true + }, + "code": { + "identifier": true, + "via": "email" } } } diff --git a/test/e2e/profiles/mobile/.kratos.yml b/test/e2e/profiles/mobile/.kratos.yml index 329c56b6f051..ab927737337d 100644 --- a/test/e2e/profiles/mobile/.kratos.yml +++ b/test/e2e/profiles/mobile/.kratos.yml @@ -9,6 +9,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true after: password: hooks: @@ -18,7 +19,6 @@ selfservice: verification: enabled: false - methods: totp: enabled: true diff --git a/test/e2e/profiles/network/.kratos.yml b/test/e2e/profiles/network/.kratos.yml index dd4e70671995..c3c8b3daedd7 100644 --- a/test/e2e/profiles/network/.kratos.yml +++ b/test/e2e/profiles/network/.kratos.yml @@ -9,6 +9,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true ui_url: http://localhost:4455/registration after: hooks: diff --git a/test/e2e/profiles/oidc-provider/.kratos.yml b/test/e2e/profiles/oidc-provider/.kratos.yml index 4850e71a380b..09b2c9978700 100644 --- a/test/e2e/profiles/oidc-provider/.kratos.yml +++ b/test/e2e/profiles/oidc-provider/.kratos.yml @@ -31,6 +31,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true ui_url: http://localhost:4455/registration after: oidc: diff --git a/test/e2e/profiles/oidc/.kratos.yml b/test/e2e/profiles/oidc/.kratos.yml index d1d6b5f696b5..b0a327bb5096 100644 --- a/test/e2e/profiles/oidc/.kratos.yml +++ b/test/e2e/profiles/oidc/.kratos.yml @@ -40,6 +40,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true ui_url: http://localhost:4455/registration after: password: diff --git a/test/e2e/profiles/oidc/identity-required.traits.schema.json b/test/e2e/profiles/oidc/identity-required.traits.schema.json index 19241c7f2760..20b2b50e9995 100644 --- a/test/e2e/profiles/oidc/identity-required.traits.schema.json +++ b/test/e2e/profiles/oidc/identity-required.traits.schema.json @@ -19,6 +19,10 @@ }, "webauthn": { "identifier": true + }, + "code": { + "identifier": true, + "via": "email" } } } diff --git a/test/e2e/profiles/oidc/identity.traits.schema.json b/test/e2e/profiles/oidc/identity.traits.schema.json index 10fb49486aed..67857c8ab987 100644 --- a/test/e2e/profiles/oidc/identity.traits.schema.json +++ b/test/e2e/profiles/oidc/identity.traits.schema.json @@ -19,6 +19,10 @@ }, "webauthn": { "identifier": true + }, + "code": { + "identifier": true, + "via": "email" } }, "verification": { diff --git a/test/e2e/profiles/passkey/.kratos.yml b/test/e2e/profiles/passkey/.kratos.yml new file mode 100644 index 000000000000..85441f599e1b --- /dev/null +++ b/test/e2e/profiles/passkey/.kratos.yml @@ -0,0 +1,55 @@ +selfservice: + flows: + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 5m + required_aal: highest_available + + logout: + after: + default_browser_return_url: http://localhost:4455/login + + registration: + enable_legacy_one_step: true + ui_url: http://localhost:4455/registration + after: + password: + hooks: + - hook: session + passkey: + hooks: + - hook: session + + login: + ui_url: http://localhost:4455/login + error: + ui_url: http://localhost:4455/error + verification: + ui_url: http://localhost:4455/verify + recovery: + ui_url: http://localhost:4455/recovery + + methods: + totp: + enabled: true + config: + issuer: issuer.ory.sh + lookup_secret: + enabled: true + passkey: + enabled: true + config: + rp: + id: localhost + origins: + - http://localhost:4455 + display_name: Ory + +identity: + schemas: + - id: default + url: file://test/e2e/profiles/passkey/identity.traits.schema.json + +session: + whoami: + required_aal: highest_available diff --git a/test/e2e/profiles/passkey/identity.traits.schema.json b/test/e2e/profiles/passkey/identity.traits.schema.json new file mode 100644 index 000000000000..8dbece14adca --- /dev/null +++ b/test/e2e/profiles/passkey/identity.traits.schema.json @@ -0,0 +1,40 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "Your E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "passkey": { + "display_name": true + } + } + } + }, + "website": { + "title": "Your website", + "type": "string", + "format": "uri", + "minLength": 10 + } + }, + "required": ["email"], + "additionalProperties": false + } + } +} diff --git a/test/e2e/profiles/passwordless/.kratos.yml b/test/e2e/profiles/passwordless/.kratos.yml index 7a7e7c5c6267..b3582a61216c 100644 --- a/test/e2e/profiles/passwordless/.kratos.yml +++ b/test/e2e/profiles/passwordless/.kratos.yml @@ -10,6 +10,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true ui_url: http://localhost:4455/registration after: password: @@ -18,6 +19,9 @@ selfservice: webauthn: hooks: - hook: session + passkey: + hooks: + - hook: session login: ui_url: http://localhost:4455/login @@ -43,6 +47,16 @@ selfservice: id: localhost origin: http://localhost:4455 display_name: Ory + passkey: + enabled: true + config: + rp: + display_name: Your Application name + # Set 'id' to the top-level domain. + id: localhost + # Set 'origin' to the exact URL of the page that prompts the user to use WebAuthn. You must include the scheme, host, and port. + origins: + - http://localhost:4455 identity: schemas: diff --git a/test/e2e/profiles/passwordless/identity.traits.schema.json b/test/e2e/profiles/passwordless/identity.traits.schema.json index 87db142ee8d2..9218e7126434 100644 --- a/test/e2e/profiles/passwordless/identity.traits.schema.json +++ b/test/e2e/profiles/passwordless/identity.traits.schema.json @@ -19,6 +19,9 @@ }, "webauthn": { "identifier": true + }, + "passkey": { + "display_name": true } } } @@ -30,10 +33,7 @@ "minLength": 10 } }, - "required": [ - "email", - "website" - ], + "required": ["email", "website"], "additionalProperties": false } } diff --git a/test/e2e/profiles/spa/.kratos.yml b/test/e2e/profiles/spa/.kratos.yml index 56590081105d..6d5eb44a67de 100644 --- a/test/e2e/profiles/spa/.kratos.yml +++ b/test/e2e/profiles/spa/.kratos.yml @@ -10,6 +10,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true ui_url: http://localhost:4455/registration after: password: diff --git a/test/e2e/profiles/two-steps/.kratos.yml b/test/e2e/profiles/two-steps/.kratos.yml new file mode 100644 index 000000000000..f5271792c0fc --- /dev/null +++ b/test/e2e/profiles/two-steps/.kratos.yml @@ -0,0 +1,109 @@ +selfservice: + flows: + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 5m + required_aal: aal1 + + logout: + after: + default_browser_return_url: http://localhost:4455/login + + registration: + enable_legacy_one_step: false + ui_url: http://localhost:4455/registration + after: + password: + hooks: + - hook: session + webauthn: + hooks: + - hook: session + code: + hooks: + - hook: session + oidc: + hooks: + - hook: session + + login: + ui_url: http://localhost:4455/login + error: + ui_url: http://localhost:4455/error + verification: + ui_url: http://localhost:4455/verify + recovery: + enabled: true + use: code + ui_url: http://localhost:4455/recovery + + methods: + password: + enabled: true + + webauthn: + enabled: true + config: + passwordless: true + rp: + display_name: Your Application name + # Set 'id' to the top-level domain. + id: localhost + # Set 'origin' to the exact URL of the page that prompts the user to use WebAuthn. You must include the scheme, host, and port. + origin: http://localhost:4455 + + totp: + config: + issuer: Kratos + enabled: true + + lookup_secret: + enabled: true + + link: + enabled: true + + code: + enabled: true + passwordless_enabled: true + config: + lifespan: 1h + + oidc: + enabled: true + config: + providers: + - id: hydra + label: Ory + provider: generic + client_id: ${OIDC_HYDRA_CLIENT_ID} + client_secret: ${OIDC_HYDRA_CLIENT_SECRET} + issuer_url: http://localhost:4444/ + scope: + - offline + mapper_url: file://test/e2e/profiles/oidc/hydra.jsonnet + - id: google + provider: generic + client_id: ${OIDC_GOOGLE_CLIENT_ID} + client_secret: ${OIDC_GOOGLE_CLIENT_SECRET} + issuer_url: http://localhost:4444/ + scope: + - offline + mapper_url: file://test/e2e/profiles/oidc/hydra.jsonnet + - id: github + provider: generic + client_id: ${OIDC_GITHUB_CLIENT_ID} + client_secret: ${OIDC_GITHUB_CLIENT_SECRET} + issuer_url: http://localhost:4444/ + scope: + - offline + mapper_url: file://test/e2e/profiles/oidc/hydra.jsonnet + +identity: + schemas: + - id: default + url: file://test/e2e/profiles/two-steps/identity.traits.schema.json + +session: + whoami: + required_aal: aal1 diff --git a/test/e2e/profiles/two-steps/identity.traits.schema.json b/test/e2e/profiles/two-steps/identity.traits.schema.json new file mode 100644 index 000000000000..51b2a1cdb91c --- /dev/null +++ b/test/e2e/profiles/two-steps/identity.traits.schema.json @@ -0,0 +1,41 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "Your E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "code": { + "identifier": true, + "via": "email" + }, + "webauthn": { + "identifier": true + } + } + } + }, + "website": { + "title": "Your website", + "type": "string", + "format": "uri", + "minLength": 10 + } + }, + "required": ["email", "website"], + "additionalProperties": false + } + } +} diff --git a/test/e2e/profiles/webhooks/.kratos.yml b/test/e2e/profiles/webhooks/.kratos.yml index f29486df20e3..00eb10537adb 100644 --- a/test/e2e/profiles/webhooks/.kratos.yml +++ b/test/e2e/profiles/webhooks/.kratos.yml @@ -9,6 +9,7 @@ selfservice: default_browser_return_url: http://localhost:4455/login registration: + enable_legacy_one_step: true ui_url: http://localhost:4455/registration after: password: diff --git a/test/e2e/proxy/package-lock.json b/test/e2e/proxy/package-lock.json index 0796984a900e..ba6fc030407c 100644 --- a/test/e2e/proxy/package-lock.json +++ b/test/e2e/proxy/package-lock.json @@ -8,7 +8,7 @@ "name": "proxy", "version": "1.0.0", "dependencies": { - "express": "4.18.2", + "express": "4.20.0", "nodemon": "2.0.22", "request": "2.88.2", "url-join": "5.0.0" @@ -115,20 +115,20 @@ } }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", + "qs": "6.13.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -137,6 +137,20 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -166,12 +180,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -244,9 +264,9 @@ } }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -280,6 +300,22 @@ "ms": "2.0.0" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -327,6 +363,25 @@ "node": ">= 0.8" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -341,36 +396,36 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", + "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.2.0", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -381,6 +436,14 @@ "node": ">= 0.10.0" } }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -483,18 +546,26 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -519,6 +590,17 @@ "node": ">= 6" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -539,17 +621,6 @@ "node": ">=6" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -558,6 +629,28 @@ "node": ">=4" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", @@ -569,6 +662,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -718,9 +822,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/methods": { "version": "1.1.2", @@ -852,9 +959,12 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -879,9 +989,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/performance-now": { "version": "2.1.0", @@ -952,9 +1062,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -1047,9 +1157,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -1075,9 +1185,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", + "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -1088,19 +1198,67 @@ "node": ">= 0.8.0" } }, + "node_modules/serve-static/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static/node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1392,22 +1550,32 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "requires": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", + "qs": "6.13.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" + }, + "dependencies": { + "qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "requires": { + "side-channel": "^1.0.6" + } + } } }, "brace-expansion": { @@ -1433,12 +1601,15 @@ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" } }, "caseless": { @@ -1488,9 +1659,9 @@ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" }, "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" }, "cookie-signature": { "version": "1.0.6", @@ -1518,6 +1689,16 @@ "ms": "2.0.0" } }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1552,6 +1733,19 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1563,41 +1757,48 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", + "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.2.0", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + } } }, "extend": { @@ -1674,18 +1875,20 @@ "optional": true }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" } }, "getpass": { @@ -1704,6 +1907,14 @@ "is-glob": "^4.0.1" } }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -1718,24 +1929,37 @@ "har-schema": "^2.0.0" } }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" + }, "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, "http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -1854,9 +2078,9 @@ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" }, "methods": { "version": "1.1.2", @@ -1950,9 +2174,9 @@ "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" }, "on-finished": { "version": "2.4.1", @@ -1968,9 +2192,9 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "performance-now": { "version": "2.1.0", @@ -2020,9 +2244,9 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "requires": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -2088,9 +2312,9 @@ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "requires": { "debug": "2.6.9", "depd": "2.0.0", @@ -2115,14 +2339,54 @@ } }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", + "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.18.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + } + } + } + }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" } }, "setprototypeof": { @@ -2131,13 +2395,14 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "simple-update-notifier": { diff --git a/test/e2e/proxy/package.json b/test/e2e/proxy/package.json index a43fd7599635..22d4da0be7f7 100644 --- a/test/e2e/proxy/package.json +++ b/test/e2e/proxy/package.json @@ -8,7 +8,7 @@ "start": "nodemon ./proxy.js" }, "dependencies": { - "express": "4.18.2", + "express": "4.20.0", "nodemon": "2.0.22", "request": "2.88.2", "url-join": "5.0.0" diff --git a/test/e2e/render-kratos-config.sh b/test/e2e/render-kratos-config.sh index 5867904216e2..b1895ecd882c 100755 --- a/test/e2e/render-kratos-config.sh +++ b/test/e2e/render-kratos-config.sh @@ -10,8 +10,8 @@ ory_x_version="$(cd $dir/../..; go list -f '{{.Version}}' -m github.com/ory/x)" curl -s https://raw.githubusercontent.com/ory/x/$ory_x_version/otelx/config.schema.json > $dir/.tracing-config.schema.json -(cd $dir; sed "s!ory://tracing-config!.tracing-config.schema.json!g;" $dir/../../embedx/config.schema.json | npx json2ts --strictIndexSignatures > $dir/cypress/support/config.d.ts) +(cd $dir; sed "s!ory://tracing-config!.tracing-config.schema.json!g;" $dir/../../embedx/config.schema.json | npx json2ts --strictIndexSignatures > $dir/shared/config.d.ts) rm $dir/.tracing-config.schema.json -make format +(cd $dir/../..; make format) diff --git a/test/e2e/run.sh b/test/e2e/run.sh index 863a70bb910b..a0a6d449fc6c 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -72,8 +72,8 @@ prepare() { if [ -z ${TEST_DATABASE_POSTGRESQL+x} ]; then docker rm -f kratos_test_database_mysql kratos_test_database_postgres kratos_test_database_cockroach || true - docker run --platform linux/amd64 --name kratos_test_database_mysql -p 3444:3306 -e MYSQL_ROOT_PASSWORD=secret -d mysql:5.7 - docker run --name kratos_test_database_postgres -p 3445:5432 -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=postgres -d postgres:9.6 postgres -c log_statement=all + docker run --name kratos_test_database_mysql -p 3444:3306 -e MYSQL_ROOT_PASSWORD=secret -d mysql:8.0 + docker run --name kratos_test_database_postgres -p 3445:5432 -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=postgres -d postgres:14 postgres -c log_statement=all docker run --name kratos_test_database_cockroach -p 3446:26257 -d cockroachdb/cockroach:v22.2.6 start-single-node --insecure export TEST_DATABASE_MYSQL="mysql://root:secret@(localhost:3444)/mysql?parseTime=true&multiStatements=true" @@ -249,11 +249,11 @@ run() { export DSN=${1} - nc -zv localhost 4434 && exit 1 - nc -zv localhost 4433 && exit 1 + nc -zv localhost 4434 && (echo "Port 4434 unavailable, used by" ; lsof -i:4434 ; exit 1) + nc -zv localhost 4433 && (echo "Port 4433 unavailable, used by" ; lsof -i:4433 ; exit 1) ls -la . - for profile in code email mobile oidc recovery recovery-mfa verification mfa spa network passwordless webhooks oidc-provider oidc-provider-mfa; do + for profile in code email mobile oidc recovery recovery-mfa verification mfa spa network passwordless passkey webhooks oidc-provider oidc-provider-mfa two-steps; do yq ea '. as $item ireduce ({}; . * $item )' test/e2e/profiles/kratos.base.yml "test/e2e/profiles/${profile}/.kratos.yml" > test/e2e/kratos.${profile}.yml cat "test/e2e/kratos.${profile}.yml" | envsubst | sponge "test/e2e/kratos.${profile}.yml" done @@ -279,7 +279,7 @@ run() { if [ -z ${CYPRESS_RECORD_KEY+x} ]; then (cd test/e2e; npm run test --) else - (cd test/e2e; npm run test -- --record) + (cd test/e2e; npm run test -- --record --tag "${2}" ) fi fi } @@ -350,19 +350,23 @@ export TEST_DATABASE_MEMORY="memory" case "${1:-default}" in sqlite) echo "Database set up at: $TEST_DATABASE_SQLITE" - db="${TEST_DATABASE_SQLITE}" + dsn="${TEST_DATABASE_SQLITE}" + db="sqlite" ;; mysql) - db="${TEST_DATABASE_MYSQL}" + dsn="${TEST_DATABASE_MYSQL}" + db="mysql" ;; postgres) - db="${TEST_DATABASE_POSTGRESQL}" + dsn="${TEST_DATABASE_POSTGRESQL}" + db="postgres" ;; cockroach) - db="${TEST_DATABASE_COCKROACHDB}" + dsn="${TEST_DATABASE_COCKROACHDB}" + db="cockroach" ;; *) @@ -380,4 +384,4 @@ if [[ "${setup}" == "yes" ]]; then prepare fi -run "${db}" +run "${dsn}" "${db}" diff --git a/test/e2e/cypress/support/config.d.ts b/test/e2e/shared/config.d.ts similarity index 88% rename from test/e2e/cypress/support/config.d.ts rename to test/e2e/shared/config.d.ts index 9e285f7928d5..a4927eb76515 100644 --- a/test/e2e/cypress/support/config.d.ts +++ b/test/e2e/shared/config.d.ts @@ -53,10 +53,18 @@ export type ProvideLoginHintsOnFailedRegistration = boolean * URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node). */ export type RegistrationUIURL = string +/** + * Two-step registration is a significantly improved sign up flow and recommended when using more than one sign up methods. To revert to one-step registration, set this to `true`. + */ +export type DisableTwoStepRegistration = boolean /** * URL where the Login UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node). */ export type LoginUIURL = string +/** + * The style of the login flow. If set to `unified` the login flow will be a one-step process. If set to `identifier_first` (experimental!) the login flow will first ask for the identifier and then the credentials. + */ +export type LoginFlowStyle = "unified" | "identifier_first" /** * If set to true will enable [Email and Phone Verification and Account Activation](https://www.ory.sh/kratos/docs/self-service/flows/verify-email-account-activation/). */ @@ -110,14 +118,6 @@ export type EnablesLinkMethod = boolean export type OverrideTheBaseURLWhichShouldBeUsedAsTheBaseForRecoveryAndVerificationLinks = string export type HowLongALinkIsValidFor = string -export type EnablesLoginAndRegistrationWithTheCodeMethod = boolean -export type EnablesLoginFlowsCodeMethodToFulfilMFARequests = boolean -/** - * This setting allows the code method to always login a user with code if they have registered with another authentication method such as password or social sign in. - */ -export type PasswordlessLoginFallbackEnabled = boolean -export type EnablesCodeMethod = boolean -export type HowLongACodeIsValidFor = string export type EnablesUsernameEmailAndPasswordMethod = boolean /** * Allows changing the default HIBP host to a self hosted version. @@ -147,6 +147,16 @@ export type MaximumPasswordLength = number * If set to false the password validation does not check for similarity between the password and the user identifier. */ export type EnablePasswordIdentifierSimilarityCheck = boolean +/** + * If set to true will enable password migration. + */ +export type EnablePasswordMigration = boolean +/** + * Define which auth mechanism the Web-Hook should use + */ +export type AuthMechanisms = + | WebHookAuthApiKeyProperties + | WebHookAuthBasicAuthProperties export type EnablesTheTOTPMethod = boolean /** * The issuer (e.g. a domain name) will be shown in the TOTP app (e.g. Google Authenticator). It helps the user differentiate between different codes. @@ -182,6 +192,19 @@ export type RelyingPartyRPConfig = origins: string[] [k: string]: unknown | undefined } +export type EnablesThePasskeyMethod = boolean +/** + * A name to help the user identify this RP. + */ +export type RelyingPartyDisplayName = string +/** + * The id must be a subset of the domain currently in the browser. + */ +export type RelyingPartyIdentifier = string +/** + * A list of explicit RP origins. If left empty, this defaults to either `origin` or `id`, prepended with the current protocol schema (HTTP or HTTPS). + */ +export type RelyingPartyOrigins = string[] export type EnablesOpenIDConnectMethod = boolean /** * Can be used to modify the base URL for OAuth2 Redirect URLs. If unset, the Public Base URL will be used. @@ -206,6 +229,7 @@ export type SelfServiceOIDCProvider = SelfServiceOIDCProvider1 & { requested_claims?: OpenIDConnectClaims organization_id?: OrganizationID additional_id_token_audiences?: AdditionalClientIdsAllowedWhenUsingIDTokenSubmission + claims_source?: ClaimsSource } export type SelfServiceOIDCProvider1 = { [k: string]: unknown | undefined @@ -213,7 +237,7 @@ export type SelfServiceOIDCProvider1 = { [k: string]: unknown | undefined } /** - * Can be one of github, github-app, gitlab, generic, google, microsoft, discord, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon. + * Can be one of github, github-app, gitlab, generic, google, microsoft, discord, salesforce, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon. */ export type Provider = | "github" @@ -223,6 +247,7 @@ export type Provider = | "google" | "microsoft" | "discord" + | "salesforce" | "slack" | "facebook" | "auth0" @@ -234,7 +259,9 @@ export type Provider = | "dingtalk" | "patreon" | "linkedin" + | "linkedin_v2" | "lark" + | "x" export type OptionalStringWhichWillBeUsedWhenGeneratingLabelsForUIButtons = string /** @@ -266,6 +293,10 @@ export type ApplePrivateKey = string */ export type OrganizationID = string export type AdditionalClientIdsAllowedWhenUsingIDTokenSubmission = string[] +/** + * Can be either `userinfo` (calls the userinfo endpoint to get the claims) or `id_token` (takes the claims from the id token). It defaults to `id_token` + */ +export type ClaimsSource = "id_token" | "userinfo" /** * A list and configuration of OAuth2 and OpenID Connect providers Ory Kratos should integrate with. */ @@ -301,7 +332,7 @@ export type HTTPAddressOfAPIEndpoint = string /** * Define which auth mechanism to use for auth with the HTTP email provider */ -export type AuthMechanisms = +export type AuthMechanisms1 = | WebHookAuthApiKeyProperties | WebHookAuthBasicAuthProperties /** @@ -339,7 +370,7 @@ export type HTTPAddressOfAPIEndpoint1 = string /** * Define which auth mechanism to use for auth with the SMS provider */ -export type AuthMechanisms1 = +export type AuthMechanisms2 = | WebHookAuthApiKeyProperties | WebHookAuthBasicAuthProperties /** @@ -521,14 +552,26 @@ export type AddExemptURLsToPrivateIPRanges = string[] * If enabled allows Ory Sessions to be cached. Only effective in the Ory Network. */ export type EnableOrySessionsCaching = boolean +/** + * Set how long Ory Sessions are cached on the edge. If unset, the session expiry will be used. Only effective in the Ory Network. + */ +export type SetOrySessionEdgeCachingMaximumAge = string /** * If enabled allows new flow transitions using `continue_with` items. */ export type EnableNewFlowTransitionsUsingContinueWithItems = boolean /** - * Secifies which organizations are available. Only effective in the Ory Network. + * If enabled allows faster session extension by skipping the session lookup. Disabling this feature will be deprecated in the future. + */ +export type EnableFasterSessionExtension = boolean +/** + * Please use selfservice.methods.b2b instead. This key will be removed. Only effective in the Ory Network. */ export type Organizations = unknown[] +/** + * A fallback URL template used when looking up identity schemas. + */ +export type FallbackURLTemplateForIdentitySchemas = string export interface OryKratosConfiguration2 { selfservice: { @@ -555,10 +598,12 @@ export interface OryKratosConfiguration2 { lifespan?: string before?: SelfServiceBeforeRegistration after?: SelfServiceAfterRegistration + enable_legacy_one_step?: DisableTwoStepRegistration } login?: { ui_url?: LoginUIURL lifespan?: string + style?: LoginFlowStyle before?: SelfServiceBeforeLogin after?: SelfServiceAfterLogin } @@ -569,6 +614,7 @@ export interface OryKratosConfiguration2 { } } methods?: { + b2b?: SingleSignOnForB2B profile?: { enabled?: EnablesProfileManagementMethod } @@ -576,13 +622,22 @@ export interface OryKratosConfiguration2 { enabled?: EnablesLinkMethod config?: LinkConfiguration } - code?: { - passwordless_enabled?: EnablesLoginAndRegistrationWithTheCodeMethod - mfa_enabled?: EnablesLoginFlowsCodeMethodToFulfilMFARequests - passwordless_login_fallback_enabled?: PasswordlessLoginFallbackEnabled - enabled?: EnablesCodeMethod - config?: CodeConfiguration - } + code?: + | { + passwordless_enabled?: true + mfa_enabled?: false + [k: string]: unknown | undefined + } + | { + mfa_enabled?: true + passwordless_enabled?: false + [k: string]: unknown | undefined + } + | { + mfa_enabled?: false + passwordless_enabled?: false + [k: string]: unknown | undefined + } password?: { enabled?: EnablesUsernameEmailAndPasswordMethod config?: PasswordConfiguration @@ -598,6 +653,10 @@ export interface OryKratosConfiguration2 { enabled?: EnablesTheWebAuthnMethod config?: WebAuthnConfiguration } + passkey?: { + enabled?: EnablesThePasskeyMethod + config?: PasskeyConfiguration + } oidc?: SpecifyOpenIDConnectAndOAuth2Configuration } } @@ -705,6 +764,16 @@ export interface OryKratosConfiguration2 { } earliest_possible_extend?: EarliestPossibleSessionExtension } + security?: { + account_enumeration?: { + /** + * Mitigate account enumeration by making it harder to figure out if an identifier (email, phone number) exists or not. Enabling this setting degrades user experience. This setting does not mitigate all possible attack vectors yet. + */ + mitigate?: boolean + [k: string]: unknown | undefined + } + [k: string]: unknown | undefined + } version?: TheKratosVersionThisConfigIsWrittenFor dev?: boolean help?: boolean @@ -724,6 +793,7 @@ export interface OryKratosConfiguration2 { clients?: GlobalOutgoingNetworkSettings feature_flags?: FeatureFlags organizations?: Organizations + enterprise?: EnterpriseFeatures } export interface SelfServiceAfterSettings { default_browser_return_url?: RedirectBrowsersToSetURLPerDefault @@ -731,6 +801,7 @@ export interface SelfServiceAfterSettings { totp?: SelfServiceAfterSettingsAuthMethod oidc?: SelfServiceAfterSettingsAuthMethod webauthn?: SelfServiceAfterSettingsAuthMethod + passkey?: SelfServiceAfterSettingsAuthMethod lookup_secret?: SelfServiceAfterSettingsAuthMethod profile?: SelfServiceAfterSettingsMethod hooks?: SelfServiceHooks @@ -766,6 +837,7 @@ export interface SelfServiceAfterRegistration { default_browser_return_url?: RedirectBrowsersToSetURLPerDefault password?: SelfServiceAfterRegistrationMethod webauthn?: SelfServiceAfterRegistrationMethod + passkey?: SelfServiceAfterRegistrationMethod oidc?: SelfServiceAfterRegistrationMethod code?: SelfServiceAfterRegistrationMethod hooks?: SelfServiceHooks @@ -792,6 +864,7 @@ export interface SelfServiceAfterLogin { default_browser_return_url?: RedirectBrowsersToSetURLPerDefault password?: SelfServiceAfterDefaultLoginMethod webauthn?: SelfServiceAfterDefaultLoginMethod + passkey?: SelfServiceAfterDefaultLoginMethod oidc?: SelfServiceAfterOIDCLoginMethod code?: SelfServiceAfterDefaultLoginMethod totp?: SelfServiceAfterDefaultLoginMethod @@ -800,6 +873,8 @@ export interface SelfServiceAfterLogin { | SelfServiceWebHook | SelfServiceSessionRevokerHook | SelfServiceRequireVerifiedAddressHook + | SelfServiceVerificationHook + | SelfServiceShowVerificationUIHook | B2BSSOHook )[] } @@ -809,11 +884,16 @@ export interface SelfServiceAfterDefaultLoginMethod { | SelfServiceSessionRevokerHook | SelfServiceRequireVerifiedAddressHook | SelfServiceWebHook + | SelfServiceVerificationHook + | SelfServiceShowVerificationUIHook )[] } export interface SelfServiceRequireVerifiedAddressHook { hook: "require_verified_address" } +export interface SelfServiceVerificationHook { + hook: "verification" +} export interface SelfServiceAfterOIDCLoginMethod { default_browser_return_url?: RedirectBrowsersToSetURLPerDefault hooks?: ( @@ -855,6 +935,25 @@ export interface SelfServiceAfterRecovery { export interface SelfServiceBeforeRecovery { hooks?: SelfServiceHooks } +/** + * Single Sign-On for B2B allows your customers to bring their own (workforce) identity server (e.g. OneLogin). This feature is not available in the open source licensed code. + */ +export interface SingleSignOnForB2B { + config?: { + organizations?: { + /** + * The ID of the organization. + */ + id?: string + /** + * The label of the organization. + */ + label?: string + domains?: string[] + [k: string]: unknown | undefined + }[] + } +} /** * Additional configuration for the link strategy. */ @@ -863,13 +962,6 @@ export interface LinkConfiguration { lifespan?: HowLongALinkIsValidFor [k: string]: unknown | undefined } -/** - * Additional configuration for the code strategy. - */ -export interface CodeConfiguration { - lifespan?: HowLongACodeIsValidFor - [k: string]: unknown | undefined -} /** * Define how passwords are validated. */ @@ -881,6 +973,61 @@ export interface PasswordConfiguration { min_password_length?: MinimumPasswordLength max_password_length?: MaximumPasswordLength identifier_similarity_check_enabled?: EnablePasswordIdentifierSimilarityCheck + migrate_hook?: { + enabled?: EnablePasswordMigration + config?: { + /** + * The URL the password migration hook should call + */ + url?: string + /** + * The HTTP method to use (GET, POST, etc). + */ + method?: "POST" + /** + * The HTTP headers that must be applied to the password migration hook. + */ + headers?: { + [k: string]: string | undefined + } + /** + * Emit tracing events for this hook on delivery or error + */ + emit_analytics_event?: boolean + auth?: AuthMechanisms + additionalProperties?: false + } + } +} +export interface WebHookAuthApiKeyProperties { + type: "api_key" + config: { + /** + * The name of the api key + */ + name: string + /** + * The value of the api key + */ + value: string + /** + * How the api key should be transferred + */ + in: "header" | "cookie" + } +} +export interface WebHookAuthBasicAuthProperties { + type: "basic_auth" + config: { + /** + * user name for basic auth + */ + user: string + /** + * password for basic auth + */ + password: string + } } export interface TOTPConfiguration { issuer?: TOTPIssuer @@ -889,6 +1036,15 @@ export interface WebAuthnConfiguration { passwordless?: UseForPasswordlessFlows rp?: RelyingPartyRPConfig } +export interface PasskeyConfiguration { + rp?: RelyingPartyRPConfig1 +} +export interface RelyingPartyRPConfig1 { + display_name: RelyingPartyDisplayName + id: RelyingPartyIdentifier + origins?: RelyingPartyOrigins + [k: string]: unknown | undefined +} export interface SpecifyOpenIDConnectAndOAuth2Configuration { enabled?: EnablesOpenIDConnectMethod config?: { @@ -968,6 +1124,7 @@ export interface CourierConfiguration { login_code?: { valid?: { email: EmailCourierTemplate + sms?: SmsCourierTemplate } } } @@ -1048,44 +1205,14 @@ export interface HttpRequestConfig { * URI pointing to the jsonnet template used for payload generation. Only used for those HTTP methods, which support HTTP body payloads */ body?: string - auth?: AuthMechanisms + auth?: AuthMechanisms1 additionalProperties?: false } -export interface WebHookAuthApiKeyProperties { - type: "api_key" - config: { - /** - * The name of the api key - */ - name: string - /** - * The value of the api key - */ - value: string - /** - * How the api key should be transferred - */ - in: "header" | "cookie" - } -} -export interface WebHookAuthBasicAuthProperties { - type: "basic_auth" - config: { - /** - * user name for basic auth - */ - user: string - /** - * password for basic auth - */ - password: string - } -} /** * Configures outgoing emails using the SMTP protocol. */ export interface SMTPConfiguration { - connection_uri: SMTPConnectionString + connection_uri?: SMTPConnectionString client_cert_path?: SMTPClientCertificatePath client_key_path?: SMTPClientPrivateKeyPath from_address?: SMTPSenderAddress @@ -1124,7 +1251,7 @@ export interface SMSSenderConfiguration { * URI pointing to the jsonnet template used for payload generation. Only used for those HTTP methods, which support HTTP body payloads */ body?: string - auth?: AuthMechanisms1 + auth?: AuthMechanisms2 additionalProperties?: false } } @@ -1380,5 +1507,13 @@ export interface GlobalHTTPClientConfiguration { } export interface FeatureFlags { cacheable_sessions?: EnableOrySessionsCaching + cacheable_sessions_max_age?: SetOrySessionEdgeCachingMaximumAge use_continue_with_transitions?: EnableNewFlowTransitionsUsingContinueWithItems + faster_session_extend?: EnableFasterSessionExtension +} +/** + * Specifies enterprise features. Only effective in the Ory Network or with a valid license. + */ +export interface EnterpriseFeatures { + identity_schema_fallback_url_template?: FallbackURLTemplateForIdentitySchemas } diff --git a/text/id.go b/text/id.go index 562a179740ef..3d9028af8ed0 100644 --- a/text/id.go +++ b/text/id.go @@ -30,6 +30,9 @@ const ( InfoSelfServiceLoginWithAndLink // 1010018 InfoSelfServiceLoginCodeMFA // 1010019 InfoSelfServiceLoginCodeMFAHint // 1010020 + InfoSelfServiceLoginPasskey // 1010021 + InfoSelfServiceLoginPassword // 1010022 + InfoSelfServiceLoginAAL2CodeAddress // 1010023 ) const ( @@ -48,6 +51,9 @@ const ( InfoSelfServiceRegistrationRegisterWebAuthn // 1040004 InfoSelfServiceRegistrationEmailWithCodeSent // 1040005 InfoSelfServiceRegistrationRegisterCode // 1040006 + InfoSelfServiceRegistrationRegisterPasskey // 1040007 + InfoSelfServiceRegistrationBack // 1040008 + InfoSelfServiceRegistrationChooseCredentials // 1040009 ) const ( @@ -70,6 +76,8 @@ const ( InfoSelfServiceSettingsDisableLookup InfoSelfServiceSettingsTOTPSecretLabel InfoSelfServiceSettingsRemoveWebAuthn + InfoSelfServiceSettingsRegisterPasskey + InfoSelfServiceSettingsRemovePasskey ) const ( @@ -80,21 +88,22 @@ const ( ) const ( - InfoNodeLabel ID = 1070000 + iota // 1070000 - InfoNodeLabelInputPassword // 1070001 - InfoNodeLabelGenerated // 1070002 - InfoNodeLabelSave // 1070003 - InfoNodeLabelID // 1070004 - InfoNodeLabelSubmit // 1070005 - InfoNodeLabelVerifyOTP // 1070006 - InfoNodeLabelEmail // 1070007 - InfoNodeLabelResendOTP // 1070008 - InfoNodeLabelContinue // 1070009 - InfoNodeLabelRecoveryCode // 1070010 - InfoNodeLabelVerificationCode // 1070011 - InfoNodeLabelRegistrationCode // 1070012 - InfoNodeLabelLoginCode // 1070013 - InfoNodeLabelLoginAndLinkCredential + InfoNodeLabel ID = 1070000 + iota // 1070000 + InfoNodeLabelInputPassword // 1070001 + InfoNodeLabelGenerated // 1070002 + InfoNodeLabelSave // 1070003 + InfoNodeLabelID // 1070004 + InfoNodeLabelSubmit // 1070005 + InfoNodeLabelVerifyOTP // 1070006 + InfoNodeLabelEmail // 1070007 + InfoNodeLabelResendOTP // 1070008 + InfoNodeLabelContinue // 1070009 + InfoNodeLabelRecoveryCode // 1070010 + InfoNodeLabelVerificationCode // 1070011 + InfoNodeLabelRegistrationCode // 1070012 + InfoNodeLabelLoginCode // 1070013 + InfoNodeLabelLoginAndLinkCredential // 1070014 + InfoNodeLabelCaptcha // 1070015 ) const ( @@ -142,6 +151,8 @@ const ( ErrorValidationPasswordTooManyBreaches ErrorValidationNoCodeUser ErrorValidationTraitsMismatch + ErrorValidationAccountNotFound + ErrorValidationCaptchaError ) const ( diff --git a/text/id_test.go b/text/id_test.go index a0190a2c0d8e..2efe742c667e 100644 --- a/text/id_test.go +++ b/text/id_test.go @@ -71,4 +71,7 @@ func TestIDs(t *testing.T) { assert.Equal(t, 1080001, int(InfoSelfServiceVerificationEmailSent)) assert.Equal(t, 1080002, int(InfoSelfServiceVerificationSuccessful)) assert.Equal(t, 1080003, int(InfoSelfServiceVerificationEmailWithCodeSent)) + + assert.Equal(t, 1070015, int(InfoNodeLabelCaptcha)) + assert.Equal(t, 4000038, int(ErrorValidationCaptchaError)) } diff --git a/text/message_login.go b/text/message_login.go index c94db4538704..f5ebc54d9899 100644 --- a/text/message_login.go +++ b/text/message_login.go @@ -56,19 +56,23 @@ func NewInfoLogin() *Message { } } -func NewInfoLoginLinkMessage(dupIdentifier, provider, newLoginURL string) *Message { +func NewInfoLoginLinkMessage(dupIdentifier, provider, newLoginURL string, availableCredentials, availableProviders []string) *Message { return &Message{ ID: InfoSelfServiceLoginLink, Type: Info, Text: fmt.Sprintf( - "Signing in will link your account to %q at provider %q. If you do not wish to link that account, please start a new login flow.", + "You tried to sign in with %q, but that email is already used by another account. Sign in to your account with one of the options below to add your account %[1]q at %q as another way to sign in.", dupIdentifier, provider, ), Context: context(map[string]any{ - "duplicateIdentifier": dupIdentifier, - "provider": provider, - "newLoginUrl": newLoginURL, + "duplicateIdentifier": dupIdentifier, + "provider": provider, + "newLoginUrl": newLoginURL, + "duplicate_identifier": dupIdentifier, + "new_login_url": newLoginURL, + "available_credential_types": availableCredentials, + "available_providers": availableProviders, }), } } @@ -89,6 +93,14 @@ func NewInfoLoginTOTP() *Message { } } +func NewInfoLoginPassword() *Message { + return &Message{ + ID: InfoSelfServiceLoginPassword, + Text: "Sign in with password", + Type: Info, + } +} + func NewInfoLoginLookup() *Message { return &Message{ ID: InfoLoginLookup, @@ -105,13 +117,14 @@ func NewInfoLoginVerify() *Message { } } -func NewInfoLoginWith(provider string) *Message { +func NewInfoLoginWith(provider string, providerId string) *Message { return &Message{ ID: InfoSelfServiceLoginWith, Text: fmt.Sprintf("Sign in with %s", provider), Type: Info, Context: context(map[string]any{ - "provider": provider, + "provider": provider, + "provider_id": providerId, }), } } @@ -119,7 +132,7 @@ func NewInfoLoginWith(provider string) *Message { func NewInfoLoginWithAndLink(provider string) *Message { return &Message{ ID: InfoSelfServiceLoginWithAndLink, - Text: fmt.Sprintf("Sign in with %s and link credential", provider), + Text: fmt.Sprintf("Confirm with %s", provider), Type: Info, Context: context(map[string]any{ "provider": provider, @@ -182,7 +195,15 @@ func NewErrorValidationVerificationNoStrategyFound() *Message { func NewInfoSelfServiceLoginWebAuthn() *Message { return &Message{ ID: InfoSelfServiceLoginWebAuthn, - Text: "Use security key", + Text: "Sign in with hardware key", + Type: Info, + } +} + +func NewInfoSelfServiceLoginPasskey() *Message { + return &Message{ + ID: InfoSelfServiceLoginPasskey, + Text: "Sign in with passkey", Type: Info, } } @@ -190,7 +211,7 @@ func NewInfoSelfServiceLoginWebAuthn() *Message { func NewInfoSelfServiceContinueLoginWebAuthn() *Message { return &Message{ ID: InfoSelfServiceLoginContinueWebAuthn, - Text: "Continue with security key", + Text: "Sign in with hardware key", Type: Info, } } @@ -231,7 +252,7 @@ func NewInfoSelfServiceLoginCode() *Message { return &Message{ ID: InfoSelfServiceLoginCode, Type: Info, - Text: "Sign in with code", + Text: "Send sign in code", } } @@ -255,17 +276,18 @@ func NewInfoSelfServiceLoginCodeMFA() *Message { return &Message{ ID: InfoSelfServiceLoginCodeMFA, Type: Info, - Text: "Continue with code", + Text: "Request code to continue", } } -func NewInfoSelfServiceLoginCodeMFAHint(maskedTo string) *Message { +func NewInfoSelfServiceLoginAAL2CodeAddress(channel string, to string) *Message { return &Message{ - ID: InfoSelfServiceLoginCodeMFAHint, + ID: InfoSelfServiceLoginAAL2CodeAddress, Type: Info, - Text: fmt.Sprintf("We will send a code to %s. To verify that this is your address please enter it here.", maskedTo), + Text: fmt.Sprintf("Send code to %s", to), Context: context(map[string]any{ - "masked_to": maskedTo, + "address": to, + "channel": channel, }), } } diff --git a/text/message_registration.go b/text/message_registration.go index 2dcf807a4766..443d234cfda7 100644 --- a/text/message_registration.go +++ b/text/message_registration.go @@ -16,13 +16,14 @@ func NewInfoRegistration() *Message { } } -func NewInfoRegistrationWith(provider string) *Message { +func NewInfoRegistrationWith(provider string, providerID string) *Message { return &Message{ ID: InfoSelfServiceRegistrationWith, Text: fmt.Sprintf("Sign up with %s", provider), Type: Info, Context: context(map[string]any{ - "provider": provider, + "provider": provider, + "provider_id": providerID, }), } } @@ -35,6 +36,22 @@ func NewInfoRegistrationContinue() *Message { } } +func NewInfoRegistrationBack() *Message { + return &Message{ + ID: InfoSelfServiceRegistrationBack, + Text: "Back", + Type: Info, + } +} + +func NewInfoSelfServiceChooseCredentials() *Message { + return &Message{ + ID: InfoSelfServiceRegistrationChooseCredentials, + Text: "Please choose a credential to authenticate yourself with.", + Type: Info, + } +} + func NewErrorValidationRegistrationFlowExpired(expiredAt time.Time) *Message { return &Message{ ID: ErrorValidationRegistrationFlowExpired, @@ -55,6 +72,14 @@ func NewInfoSelfServiceRegistrationRegisterWebAuthn() *Message { } } +func NewInfoSelfServiceRegistrationRegisterPasskey() *Message { + return &Message{ + ID: InfoSelfServiceRegistrationRegisterPasskey, + Text: "Sign up with passkey", + Type: Info, + } +} + func NewRegistrationEmailWithCodeSent() *Message { return &Message{ ID: InfoSelfServiceRegistrationEmailWithCodeSent, @@ -82,7 +107,7 @@ func NewErrorValidationRegistrationRetrySuccessful() *Message { func NewInfoSelfServiceRegistrationRegisterCode() *Message { return &Message{ ID: InfoSelfServiceRegistrationRegisterCode, - Text: "Sign up with code", + Text: "Send sign up code", Type: Info, } } diff --git a/text/message_settings.go b/text/message_settings.go index 9cfeb370c33c..97f0ff0266c6 100644 --- a/text/message_settings.go +++ b/text/message_settings.go @@ -166,6 +166,14 @@ func NewInfoSelfServiceSettingsRegisterWebAuthn() *Message { } } +func NewInfoSelfServiceSettingsRegisterPasskey() *Message { + return &Message{ + ID: InfoSelfServiceSettingsRegisterPasskey, + Text: "Add passkey", + Type: Info, + } +} + func NewInfoSelfServiceRegisterWebAuthnDisplayName() *Message { return &Message{ ID: InfoSelfServiceSettingsRegisterWebAuthnDisplayName, @@ -186,3 +194,16 @@ func NewInfoSelfServiceRemoveWebAuthn(name string, createdAt time.Time) *Message }), } } + +func NewInfoSelfServiceRemovePasskey(name string, createdAt time.Time) *Message { + return &Message{ + ID: InfoSelfServiceSettingsRemovePasskey, + Text: fmt.Sprintf("Remove passkey \"%s\"", name), + Type: Info, + Context: context(map[string]any{ + "display_name": name, + "added_at": createdAt, + "added_at_unix": createdAt.Unix(), + }), + } +} diff --git a/text/message_system.go b/text/message_system.go index d94ce73f9872..07f30374495a 100644 --- a/text/message_system.go +++ b/text/message_system.go @@ -13,3 +13,11 @@ func NewErrorSystemGeneric(reason string) *Message { }), } } + +func NewCaptchaContainerMessage() *Message { + return &Message{ + ID: InfoNodeLabelCaptcha, + Text: "Please complete the captcha challenge to continue.", + Type: Info, + } +} diff --git a/text/message_validation.go b/text/message_validation.go index 28396180c0c5..b8e689deee03 100644 --- a/text/message_validation.go +++ b/text/message_validation.go @@ -9,8 +9,6 @@ import ( "golang.org/x/text/cases" "golang.org/x/text/language" - - "golang.org/x/exp/maps" ) func NewValidationErrorGeneric(reason string) *Message { @@ -259,6 +257,14 @@ func NewErrorValidationInvalidCredentials() *Message { } } +func NewErrorValidationAccountNotFound() *Message { + return &Message{ + ID: ErrorValidationAccountNotFound, + Text: "This account does not exist or has no login method configured.", + Type: Error, + } +} + func NewErrorValidationDuplicateCredentials() *Message { return &Message{ ID: ErrorValidationDuplicateCredentials, @@ -279,25 +285,32 @@ func NewErrorValidationDuplicateCredentialsWithHints(availableCredentialTypes [] reason := fmt.Sprintf("You tried signing in with %s which is already in use by another account.", identifier) if len(availableCredentialTypes) > 0 { - humanReadable := make(map[string]struct{}, len(availableCredentialTypes)) + humanReadable := make([]string, 0, len(availableCredentialTypes)) for _, cred := range availableCredentialTypes { switch cred { case "password": - humanReadable["your password"] = struct{}{} + humanReadable = append(humanReadable, "your password") case "oidc": - humanReadable["social sign in"] = struct{}{} + humanReadable = append(humanReadable, "social sign in") case "webauthn": - humanReadable["your PassKey or a security key"] = struct{}{} + humanReadable = append(humanReadable, "your passkey or a security key") + case "passkey": + humanReadable = append(humanReadable, "your passkey") } } if len(humanReadable) == 0 { // show at least some hint // also our example message generation tool runs into this case - for _, cred := range availableCredentialTypes { - humanReadable[cred] = struct{}{} - } + humanReadable = append(humanReadable, availableCredentialTypes...) + } + + // Final format: "You can sign in using foo, bar, or baz." + if len(humanReadable) > 1 { + humanReadable[len(humanReadable)-1] = "or " + humanReadable[len(humanReadable)-1] + } + if len(humanReadable) > 0 { + reason += fmt.Sprintf(" You can sign in using %s.", strings.Join(humanReadable, ", ")) } - reason += fmt.Sprintf(" You can sign in using %s.", strings.Join(maps.Keys(humanReadable), ", ")) } if len(oidcProviders) > 0 { reason += fmt.Sprintf(" You can sign in using one of the following social sign in providers: %s.", strings.Join(oidcProviders, ", ")) @@ -411,3 +424,11 @@ func NewErrorValidationTraitsMismatch() *Message { Type: Error, } } + +func NewErrorCaptchaFailed() *Message { + return &Message{ + ID: ErrorValidationCaptchaError, + Text: "Captcha verification failed, please try again.", + Type: Error, + } +} diff --git a/ui/node/attributes.go b/ui/node/attributes.go index dd36e6e675e1..6210b3ba7940 100644 --- a/ui/node/attributes.go +++ b/ui/node/attributes.go @@ -3,7 +3,12 @@ package node -import "github.com/ory/kratos/text" +import ( + "fmt" + + "github.com/ory/kratos/text" + "github.com/ory/kratos/x/webauthnx/js" +) const ( InputAttributeTypeText UiNodeInputAttributeType = "text" @@ -53,6 +58,9 @@ type Attributes interface { // swagger:ignore GetNodeType() UiNodeType + + // swagger:ignore + Matches(other Attributes) bool } // InputAttributes represents the attributes of an input node @@ -97,13 +105,34 @@ type InputAttributes struct { // OnClick may contain javascript which should be executed on click. This is primarily // used for WebAuthn. + // + // Deprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead. OnClick string `json:"onclick,omitempty"` + // OnClickTrigger may contain a WebAuthn trigger which should be executed on click. + // + // The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. + OnClickTrigger js.WebAuthnTriggers `json:"onclickTrigger,omitempty"` + + // OnLoad may contain javascript which should be executed on load. This is primarily + // used for WebAuthn. + // + // Deprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead. + OnLoad string `json:"onload,omitempty"` + + // OnLoadTrigger may contain a WebAuthn trigger which should be executed on load. + // + // The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. + OnLoadTrigger js.WebAuthnTriggers `json:"onloadTrigger,omitempty"` + + // MaxLength may contain the input's maximum length. + MaxLength int `json:"maxlength,omitempty"` + // NodeType represents this node's types. It is a mirror of `node.type` and // is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "input". // // required: true - NodeType string `json:"node_type"` + NodeType UiNodeType `json:"node_type"` } // ImageAttributes represents the attributes of an image node. @@ -135,7 +164,7 @@ type ImageAttributes struct { // is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "img". // // required: true - NodeType string `json:"node_type"` + NodeType UiNodeType `json:"node_type"` } // AnchorAttributes represents the attributes of an anchor node. @@ -162,7 +191,7 @@ type AnchorAttributes struct { // is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "a". // // required: true - NodeType string `json:"node_type"` + NodeType UiNodeType `json:"node_type"` } // TextAttributes represents the attributes of a text node. @@ -183,7 +212,7 @@ type TextAttributes struct { // is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "text". // // required: true - NodeType string `json:"node_type"` + NodeType UiNodeType `json:"node_type"` } // ScriptAttributes represent script nodes which load javascript. @@ -238,7 +267,7 @@ type ScriptAttributes struct { // is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "script". // // required: true - NodeType string `json:"node_type"` + NodeType UiNodeType `json:"node_type"` } var ( @@ -269,6 +298,99 @@ func (a *ScriptAttributes) ID() string { return a.Identifier } +func (a *InputAttributes) Matches(other Attributes) bool { + ot, ok := other.(*InputAttributes) + if !ok { + return false + } + + if len(ot.ID()) > 0 && a.ID() != ot.ID() { + return false + } + + if len(ot.Type) > 0 && a.Type != ot.Type { + return false + } + + if ot.FieldValue != nil && fmt.Sprintf("%v", a.FieldValue) != fmt.Sprintf("%v", ot.FieldValue) { + return false + } + + if len(ot.Name) > 0 && a.Name != ot.Name { + return false + } + + return true +} + +func (a *ImageAttributes) Matches(other Attributes) bool { + ot, ok := other.(*ImageAttributes) + if !ok { + return false + } + + if len(ot.ID()) > 0 && a.ID() != ot.ID() { + return false + } + + if len(ot.Source) > 0 && a.Source != ot.Source { + return false + } + + return true +} + +func (a *AnchorAttributes) Matches(other Attributes) bool { + ot, ok := other.(*AnchorAttributes) + if !ok { + return false + } + + if len(ot.ID()) > 0 && a.ID() != ot.ID() { + return false + } + + if len(ot.HREF) > 0 && a.HREF != ot.HREF { + return false + } + + return true +} + +func (a *TextAttributes) Matches(other Attributes) bool { + ot, ok := other.(*TextAttributes) + if !ok { + return false + } + + if len(ot.ID()) > 0 && a.ID() != ot.ID() { + return false + } + + return true +} + +func (a *ScriptAttributes) Matches(other Attributes) bool { + ot, ok := other.(*ScriptAttributes) + if !ok { + return false + } + + if len(ot.ID()) > 0 && a.ID() != ot.ID() { + return false + } + + if ot.Type != "" && a.Type != ot.Type { + return false + } + + if ot.Source != "" && a.Source != ot.Source { + return false + } + + return true +} + func (a *InputAttributes) SetValue(value interface{}) { a.FieldValue = value } diff --git a/ui/node/attributes_input.go b/ui/node/attributes_input.go index 78e8a84d2897..176c1a25b42e 100644 --- a/ui/node/attributes_input.go +++ b/ui/node/attributes_input.go @@ -38,6 +38,12 @@ func WithRequiredInputAttribute(a *InputAttributes) { a.Required = true } +func WithMaxLengthInputAttribute(maxLength int) func(a *InputAttributes) { + return func(a *InputAttributes) { + a.MaxLength = maxLength + } +} + func WithInputAttributes(f func(a *InputAttributes)) func(a *InputAttributes) { return func(a *InputAttributes) { f(a) @@ -70,12 +76,6 @@ func applyImageAttributes(opts ImageAttributesModifiers, attributes *ImageAttrib type ScriptAttributesModifier func(attributes *ScriptAttributes) type ScriptAttributesModifiers []ScriptAttributesModifier -func WithScriptAttributes(f func(a *ScriptAttributes)) func(a *ScriptAttributes) { - return func(a *ScriptAttributes) { - f(a) - } -} - func applyScriptAttributes(opts ScriptAttributesModifiers, attributes *ScriptAttributes) *ScriptAttributes { for _, f := range opts { f(attributes) diff --git a/ui/node/attributes_test.go b/ui/node/attributes_test.go index 218919e1145e..62c2316d3c9f 100644 --- a/ui/node/attributes_test.go +++ b/ui/node/attributes_test.go @@ -21,6 +21,70 @@ func TestIDs(t *testing.T) { assert.EqualValues(t, "foo", (&ScriptAttributes{Identifier: "foo"}).ID()) } +func TestMatchesAnchorAttributes(t *testing.T) { + assert.True(t, (&AnchorAttributes{Identifier: "foo"}).Matches(&AnchorAttributes{Identifier: "foo"})) + assert.True(t, (&AnchorAttributes{HREF: "bar"}).Matches(&AnchorAttributes{HREF: "bar"})) + assert.False(t, (&AnchorAttributes{HREF: "foo"}).Matches(&AnchorAttributes{HREF: "bar"})) + assert.False(t, (&AnchorAttributes{Identifier: "foo"}).Matches(&AnchorAttributes{HREF: "bar"})) + + assert.True(t, (&AnchorAttributes{Identifier: "foo", HREF: "bar"}).Matches(&AnchorAttributes{Identifier: "foo", HREF: "bar"})) + assert.False(t, (&AnchorAttributes{Identifier: "foo", HREF: "bar"}).Matches(&AnchorAttributes{Identifier: "foo", HREF: "baz"})) + assert.False(t, (&AnchorAttributes{Identifier: "foo", HREF: "bar"}).Matches(&AnchorAttributes{Identifier: "bar", HREF: "bar"})) + + assert.False(t, (&AnchorAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"})) +} + +func TestMatchesImageAttributes(t *testing.T) { + assert.True(t, (&ImageAttributes{Identifier: "foo"}).Matches(&ImageAttributes{Identifier: "foo"})) + assert.True(t, (&ImageAttributes{Source: "bar"}).Matches(&ImageAttributes{Source: "bar"})) + assert.False(t, (&ImageAttributes{Source: "foo"}).Matches(&ImageAttributes{Source: "bar"})) + assert.False(t, (&ImageAttributes{Identifier: "foo"}).Matches(&ImageAttributes{Source: "bar"})) + + assert.True(t, (&ImageAttributes{Identifier: "foo", Source: "bar"}).Matches(&ImageAttributes{Identifier: "foo", Source: "bar"})) + assert.False(t, (&ImageAttributes{Identifier: "foo", Source: "bar"}).Matches(&ImageAttributes{Identifier: "foo", Source: "baz"})) + assert.False(t, (&ImageAttributes{Identifier: "foo", Source: "bar"}).Matches(&ImageAttributes{Identifier: "bar", Source: "bar"})) + + assert.False(t, (&ImageAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"})) +} + +func TestMatchesInputAttributes(t *testing.T) { + // Test when other is not of type *InputAttributes + var attr Attributes = &ImageAttributes{} + inputAttr := &InputAttributes{Name: "foo"} + assert.False(t, inputAttr.Matches(attr)) + + // Test when ID is different + attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText} + inputAttr = &InputAttributes{Name: "bar", Type: InputAttributeTypeText} + assert.False(t, inputAttr.Matches(attr)) + + // Test when Type is different + attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText} + inputAttr = &InputAttributes{Name: "foo", Type: InputAttributeTypeNumber} + assert.False(t, inputAttr.Matches(attr)) + + // Test when FieldValue is different + attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "bar"} + inputAttr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "baz"} + assert.False(t, inputAttr.Matches(attr)) + + // Test when Name is different + attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText} + inputAttr = &InputAttributes{Name: "bar", Type: InputAttributeTypeText} + assert.False(t, inputAttr.Matches(attr)) + + // Test when all fields are the same + attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "bar"} + inputAttr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "bar"} + assert.True(t, inputAttr.Matches(attr)) +} + +func TestMatchesTextAttributes(t *testing.T) { + assert.True(t, (&TextAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"})) + assert.True(t, (&TextAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"})) + assert.False(t, (&TextAttributes{Identifier: "foo"}).Matches(&ImageAttributes{Identifier: "foo"})) +} + func TestNodeEncode(t *testing.T) { script := jsonx.TestMarshalJSONString(t, &Node{Attributes: &ScriptAttributes{}}) assert.EqualValues(t, Script, gjson.Get(script, "attributes.node_type").String()) diff --git a/ui/node/identifiers.go b/ui/node/identifiers.go index 17ed2dd88c27..16cf2e1acc4e 100644 --- a/ui/node/identifiers.go +++ b/ui/node/identifiers.go @@ -19,6 +19,10 @@ const ( LookupCodeEnter = "lookup_secret" ) +const ( + ProfileChooseCredentials = "profile_choose_credentials" +) + const ( WebAuthnRegisterTrigger = "webauthn_register_trigger" WebAuthnRegister = "webauthn_register" @@ -28,3 +32,14 @@ const ( WebAuthnRemove = "webauthn_remove" WebAuthnScript = "webauthn_script" ) + +const ( + PasskeyRegisterTrigger = "passkey_register_trigger" + PasskeyRegister = "passkey_register" + PasskeySettingsRegister = "passkey_settings_register" + PasskeyCreateData = "passkey_create_data" + PasskeyLogin = "passkey_login" + PasskeyChallenge = "passkey_challenge" + PasskeyLoginTrigger = "passkey_login_trigger" //#nosec G101 -- Not a credential + PasskeyRemove = "passkey_remove" +) diff --git a/ui/node/node.go b/ui/node/node.go index c1c2aa64f1c0..66e1b72fa8b6 100644 --- a/ui/node/node.go +++ b/ui/node/node.go @@ -39,15 +39,17 @@ func (t UiNodeType) String() string { type UiNodeGroup string const ( - DefaultGroup UiNodeGroup = "default" - PasswordGroup UiNodeGroup = "password" - OpenIDConnectGroup UiNodeGroup = "oidc" - ProfileGroup UiNodeGroup = "profile" - LinkGroup UiNodeGroup = "link" - CodeGroup UiNodeGroup = "code" - TOTPGroup UiNodeGroup = "totp" - LookupGroup UiNodeGroup = "lookup_secret" - WebAuthnGroup UiNodeGroup = "webauthn" + DefaultGroup UiNodeGroup = "default" + PasswordGroup UiNodeGroup = "password" + OpenIDConnectGroup UiNodeGroup = "oidc" + ProfileGroup UiNodeGroup = "profile" + LinkGroup UiNodeGroup = "link" + CodeGroup UiNodeGroup = "code" + TOTPGroup UiNodeGroup = "totp" + LookupGroup UiNodeGroup = "lookup_secret" + WebAuthnGroup UiNodeGroup = "webauthn" + PasskeyGroup UiNodeGroup = "passkey" + IdentifierFirstGroup UiNodeGroup = "identifier_first" ) func (g UiNodeGroup) String() string { @@ -217,6 +219,7 @@ func SortUseOrder(keysInOrder []string) func(*sortOptions) { options.keysInOrder = keysInOrder } } + func SortUseOrderAppend(keysInOrder []string) func(*sortOptions) { return func(options *sortOptions) { options.keysInOrderAppend = keysInOrder @@ -352,28 +355,59 @@ func (n *Nodes) Append(node *Node) { *n = append(*n, node) } +func (n *Nodes) RemoveMatching(node *Node) { + if n == nil { + return + } + + var r Nodes + for k, v := range *n { + if !(*n)[k].Matches(node) { + r = append(r, v) + } + } + + *n = r +} + +func (n *Node) Matches(needle *Node) bool { + if len(needle.ID()) > 0 && n.ID() != needle.ID() { + return false + } + + if needle.Type != "" && n.Type != needle.Type { + return false + } + + if needle.Group != "" && n.Group != needle.Group { + return false + } + + return n.Attributes.Matches(needle.Attributes) +} + func (n *Node) UnmarshalJSON(data []byte) error { var attr Attributes switch t := gjson.GetBytes(data, "type").String(); UiNodeType(t) { case Text: attr = &TextAttributes{ - NodeType: string(Text), + NodeType: Text, } case Input: attr = &InputAttributes{ - NodeType: string(Input), + NodeType: Input, } case Anchor: attr = &AnchorAttributes{ - NodeType: string(Anchor), + NodeType: Anchor, } case Image: attr = &ImageAttributes{ - NodeType: string(Image), + NodeType: Image, } case Script: attr = &ScriptAttributes{ - NodeType: string(Script), + NodeType: Script, } default: return fmt.Errorf("unexpected node type: %s", t) @@ -400,19 +434,19 @@ func (n *Node) MarshalJSON() ([]byte, error) { switch attr := n.Attributes.(type) { case *TextAttributes: t = Text - attr.NodeType = string(Text) + attr.NodeType = Text case *InputAttributes: t = Input - attr.NodeType = string(Input) + attr.NodeType = Input case *AnchorAttributes: t = Anchor - attr.NodeType = string(Anchor) + attr.NodeType = Anchor case *ImageAttributes: t = Image - attr.NodeType = string(Image) + attr.NodeType = Image case *ScriptAttributes: t = Script - attr.NodeType = string(Script) + attr.NodeType = Script default: return nil, errors.WithStack(fmt.Errorf("unknown node type: %T", n.Attributes)) } diff --git a/ui/node/node_test.go b/ui/node/node_test.go index f8867b98c2a3..e8a85bc9cd9f 100644 --- a/ui/node/node_test.go +++ b/ui/node/node_test.go @@ -11,6 +11,8 @@ import ( "path/filepath" "testing" + "github.com/ory/kratos/text" + "github.com/ory/x/assertx" "github.com/ory/kratos/corpx" @@ -193,3 +195,64 @@ func TestNodeJSON(t *testing.T) { require.EqualError(t, json.NewDecoder(bytes.NewReader(json.RawMessage(`{"type": "foo"}`))).Decode(&n), "unexpected node type: foo") }) } + +func TestMatchesNode(t *testing.T) { + // Test when ID is different + node1 := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}} + node2 := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}} + assert.False(t, node1.Matches(node2)) + + // Test when Type is different + node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}} + node2 = &node.Node{Type: node.Text, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}} + assert.False(t, node1.Matches(node2)) + + // Test when Group is different + node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}} + node2 = &node.Node{Type: node.Input, Group: node.OpenIDConnectGroup, Attributes: &node.InputAttributes{Name: "foo"}} + assert.False(t, node1.Matches(node2)) + + // Test when all fields are the same + node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}} + node2 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}} + assert.True(t, node1.Matches(node2)) +} + +func TestRemoveMatchingNodes(t *testing.T) { + nodes := node.Nodes{ + &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}, + &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}}, + &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "baz"}}, + } + + // Test when node to remove is present + nodeToRemove := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}} + nodes.RemoveMatching(nodeToRemove) + assert.Len(t, nodes, 2) + for _, n := range nodes { + assert.NotEqual(t, nodeToRemove.ID(), n.ID()) + } + + // Test when node to remove is not present + nodeToRemove = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "qux"}} + nodes.RemoveMatching(nodeToRemove) + assert.Len(t, nodes, 2) // length should remain the same + + // Test when node to remove is present + nodeToRemove = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "baz"}} + ui := &container.Container{ + Nodes: nodes, + } + + ui.GetNodes().RemoveMatching(nodeToRemove) + assert.Len(t, *ui.GetNodes(), 1) + for _, n := range *ui.GetNodes() { + assert.NotEqual(t, "bar", n.ID()) + assert.NotEqual(t, "baz", n.ID()) + } + + ui.Nodes.Append(node.NewInputField("method", "foo", "bar", node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue())) + assert.NotNil(t, ui.Nodes.Find("method")) + ui.GetNodes().RemoveMatching(node.NewInputField("method", "foo", "bar", node.InputAttributeTypeSubmit)) + assert.Nil(t, ui.Nodes.Find("method")) +} diff --git a/x/cookie.go b/x/cookie.go index 4172d2878cf5..897401183c0e 100644 --- a/x/cookie.go +++ b/x/cookie.go @@ -5,6 +5,7 @@ package x import ( "net/http" + "time" "github.com/gorilla/sessions" "github.com/pkg/errors" @@ -71,6 +72,17 @@ func SessionUnset(w http.ResponseWriter, r *http.Request, s sessions.StoreExact, return errors.WithStack(cookie.Save(r, w)) } +func SessionSetExpiresIn(w http.ResponseWriter, r *http.Request, s sessions.StoreExact, id string, expiresIn time.Duration) error { + cookie, err := s.Get(r, id) + if err == nil && cookie.IsNew { + // No cookie was sent in the request. We have nothing to do. + return nil + } + + cookie.Options.MaxAge = int(expiresIn.Seconds()) + return errors.WithStack(cookie.Save(r, w)) +} + func SessionUnsetKey(w http.ResponseWriter, r *http.Request, s sessions.StoreExact, id, key string) error { cookie, err := s.Get(r, id) if err == nil && cookie.IsNew { diff --git a/x/err.go b/x/err.go index 2a42b3eb540b..5b3868734cca 100644 --- a/x/err.go +++ b/x/err.go @@ -10,12 +10,15 @@ import ( "github.com/ory/herodot" ) -var PseudoPanic = herodot.DefaultError{ - StatusField: http.StatusText(http.StatusInternalServerError), - ErrorField: "Code Bug Detected", - ReasonField: "The code ended up at a place where it should not have. Please report this as an issue at https://github.com/ory/kratos", - CodeField: http.StatusInternalServerError, -} +var ( + PseudoPanic = herodot.DefaultError{ + StatusField: http.StatusText(http.StatusInternalServerError), + ErrorField: "Code Bug Detected", + ReasonField: "The code ended up at a place where it should not have. Please report this as an issue at https://github.com/ory/kratos", + CodeField: http.StatusInternalServerError, + } + PageTokenInvalid = herodot.ErrBadRequest.WithReason("The page token is invalid, do not craft your own page tokens") +) func RecoverStatusCode(err error, fallback int) int { var sc herodot.StatusCodeCarrier diff --git a/x/events/events.go b/x/events/events.go index 52e921112d12..862a35a39e98 100644 --- a/x/events/events.go +++ b/x/events/events.go @@ -16,31 +16,34 @@ import ( ) const ( - SessionIssued semconv.Event = "SessionIssued" - SessionChanged semconv.Event = "SessionChanged" - SessionRevoked semconv.Event = "SessionRevoked" - SessionChecked semconv.Event = "SessionChecked" - SessionTokenizedAsJWT semconv.Event = "SessionTokenizedAsJWT" - RegistrationFailed semconv.Event = "RegistrationFailed" - RegistrationSucceeded semconv.Event = "RegistrationSucceeded" - LoginFailed semconv.Event = "LoginFailed" - LoginSucceeded semconv.Event = "LoginSucceeded" - SettingsFailed semconv.Event = "SettingsFailed" - SettingsSucceeded semconv.Event = "SettingsSucceeded" - RecoveryFailed semconv.Event = "RecoveryFailed" - RecoverySucceeded semconv.Event = "RecoverySucceeded" - VerificationFailed semconv.Event = "VerificationFailed" - VerificationSucceeded semconv.Event = "VerificationSucceeded" - IdentityCreated semconv.Event = "IdentityCreated" - IdentityUpdated semconv.Event = "IdentityUpdated" - WebhookDelivered semconv.Event = "WebhookDelivered" - WebhookSucceeded semconv.Event = "WebhookSucceeded" - WebhookFailed semconv.Event = "WebhookFailed" + SessionIssued semconv.Event = "SessionIssued" + SessionChanged semconv.Event = "SessionChanged" + SessionLifespanExtended semconv.Event = "SessionLifespanExtended" + SessionRevoked semconv.Event = "SessionRevoked" + SessionChecked semconv.Event = "SessionChecked" + SessionTokenizedAsJWT semconv.Event = "SessionTokenizedAsJWT" + RegistrationFailed semconv.Event = "RegistrationFailed" + RegistrationSucceeded semconv.Event = "RegistrationSucceeded" + LoginFailed semconv.Event = "LoginFailed" + LoginSucceeded semconv.Event = "LoginSucceeded" + SettingsFailed semconv.Event = "SettingsFailed" + SettingsSucceeded semconv.Event = "SettingsSucceeded" + RecoveryFailed semconv.Event = "RecoveryFailed" + RecoverySucceeded semconv.Event = "RecoverySucceeded" + VerificationFailed semconv.Event = "VerificationFailed" + VerificationSucceeded semconv.Event = "VerificationSucceeded" + IdentityCreated semconv.Event = "IdentityCreated" + IdentityUpdated semconv.Event = "IdentityUpdated" + IdentityDeleted semconv.Event = "IdentityDeleted" + WebhookDelivered semconv.Event = "WebhookDelivered" + WebhookSucceeded semconv.Event = "WebhookSucceeded" + WebhookFailed semconv.Event = "WebhookFailed" ) const ( attributeKeySessionID semconv.AttributeKey = "SessionID" attributeKeySessionAAL semconv.AttributeKey = "SessionAAL" + attributeKeySessionExpiresAt semconv.AttributeKey = "SessionExpiresAt" attributeKeySelfServiceFlowType semconv.AttributeKey = "SelfServiceFlowType" attributeKeySelfServiceMethodUsed semconv.AttributeKey = "SelfServiceMethodUsed" attributeKeySelfServiceSSOProviderUsed semconv.AttributeKey = "SelfServiceSSOProviderUsed" @@ -71,6 +74,10 @@ func attLoginRequestedAAL(val string) otelattr.KeyValue { return otelattr.String(attributeKeyLoginRequestedAAL.String(), val) } +func attSessionExpiresAt(expiresAt time.Time) otelattr.KeyValue { + return otelattr.String(attributeKeySessionExpiresAt.String(), expiresAt.String()) +} + func attLoginRequestedPrivilegedSession(val bool) otelattr.KeyValue { return otelattr.Bool(attributeKeyLoginRequestedPrivilegedSession.String(), val) } @@ -135,6 +142,18 @@ func NewSessionChanged(ctx context.Context, aal string, sessionID, identityID uu ) } +func NewSessionLifespanExtended(ctx context.Context, sessionID, identityID uuid.UUID, newExpiry time.Time) (string, trace.EventOption) { + return SessionLifespanExtended.String(), + trace.WithAttributes( + append( + semconv.AttributesFromContext(ctx), + semconv.AttrIdentityID(identityID), + attrSessionID(sessionID), + attSessionExpiresAt(newExpiry), + )..., + ) +} + type LoginSucceededOpts struct { SessionID, IdentityID uuid.UUID FlowType, RequestedAAL, Method, SSOProvider string @@ -244,6 +263,16 @@ func NewIdentityCreated(ctx context.Context, identityID uuid.UUID) (string, trac ) } +func NewIdentityDeleted(ctx context.Context, identityID uuid.UUID) (string, trace.EventOption) { + return IdentityDeleted.String(), + trace.WithAttributes( + append( + semconv.AttributesFromContext(ctx), + semconv.AttrIdentityID(identityID), + )..., + ) +} + func NewIdentityUpdated(ctx context.Context, identityID uuid.UUID) (string, trace.EventOption) { return IdentityUpdated.String(), trace.WithAttributes( diff --git a/x/http_secure_redirect.go b/x/http_secure_redirect.go index 9851abc0f53e..1b86b00940db 100644 --- a/x/http_secure_redirect.go +++ b/x/http_secure_redirect.go @@ -17,8 +17,6 @@ import ( "github.com/ory/x/stringsx" "github.com/ory/x/urlx" - "github.com/samber/lo" - "github.com/ory/kratos/driver/config" ) @@ -145,10 +143,8 @@ func SecureRedirectTo(r *http.Request, defaultReturnTo *url.URL, opts ...SecureR return nil, errors.WithStack(herodot.ErrBadRequest. WithID(text.ErrIDRedirectURLNotAllowed). - WithReasonf("Requested return_to URL %q is not allowed.", returnTo). - WithDebugf("Allowed domains are: %v", strings.Join(lo.Map(o.allowlist, func(u url.URL, _ int) string { - return u.String() - }), ", "))) + WithReasonf("Requested return_to URL %q is not allowed.", returnTo), + ) } func SecureContentNegotiationRedirection( diff --git a/x/json_marshal.go b/x/json_marshal.go new file mode 100644 index 000000000000..65f2ef089bd1 --- /dev/null +++ b/x/json_marshal.go @@ -0,0 +1,18 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import "encoding/json" + +// ParseRawMessageOrEmpty parses a json.RawMessage and returns an empty map if the input is empty. +func ParseRawMessageOrEmpty(input json.RawMessage) (map[string]interface{}, error) { + if len(input) == 0 { + return map[string]interface{}{}, nil + } + var m map[string]interface{} + if err := json.Unmarshal(input, &m); err != nil { + return nil, err + } + return m, nil +} diff --git a/x/json_marshal_test.go b/x/json_marshal_test.go new file mode 100644 index 000000000000..4484699a3fdf --- /dev/null +++ b/x/json_marshal_test.go @@ -0,0 +1,52 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/x" +) + +func TestParseRawMessageOrEmpty(t *testing.T) { + for _, tc := range []struct { + input json.RawMessage + expect map[string]interface{} + err any + }{ + { + input: json.RawMessage("invalid json"), + err: "invalid character 'i' looking for beginning of value", + }, + { + input: json.RawMessage(""), + expect: map[string]interface{}{}, + }, + { + input: json.RawMessage(`{"foo": "bar"}`), + expect: map[string]interface{}{ + "foo": "bar", + }, + }, + { + input: json.RawMessage(`{"foo": "b`), + err: "unexpected end of JSON input", + }, + } { + t.Run(fmt.Sprintf("with input '%s'", tc.input), func(t *testing.T) { + m, err := x.ParseRawMessageOrEmpty(tc.input) + if tc.err != nil { + require.Error(t, err) + require.Equal(t, tc.err, err.Error()) + return + } + require.NoError(t, err) + require.Equal(t, tc.expect, m) + }) + } +} diff --git a/x/mailhog.go b/x/mailhog.go index 8a54f5a35ed4..6f5537ba73d8 100644 --- a/x/mailhog.go +++ b/x/mailhog.go @@ -32,7 +32,7 @@ func CleanUpTestSMTP() { resources = nil } -func RunTestSMTP() (smtp, api string, err error) { +func RunTestSMTP(options ...string) (smtp, api string, err error) { if smtp, api := os.Getenv("TEST_MAILHOG_SMTP"), os.Getenv("TEST_MAILHOG_API"); smtp != "" && api != "" { return smtp, api, nil } else if len(smtp)+len(api) > 0 { @@ -53,20 +53,24 @@ func RunTestSMTP() (smtp, api string, err error) { } smtpPort, apiPort := ports[0], ports[1] + if len(options) == 0 { + options = []string{ + "-invite-jim", + "-jim-linkspeed-affect=0.05", + "-jim-reject-auth=0.05", + "-jim-reject-recipient=0.05", + "-jim-reject-sender=0.05", + "-jim-disconnect=0.05", + "-jim-linkspeed-min=1250", + "-jim-linkspeed-max=12500", + } + } + resource, err := pool. RunWithOptions(&dockertest.RunOptions{ Repository: "mailhog/mailhog", Tag: "v1.0.0", - Cmd: []string{ - "-invite-jim", - "-jim-linkspeed-affect=0.05", - "-jim-reject-auth=0.05", - "-jim-reject-recipient=0.05", - "-jim-reject-sender=0.05", - "-jim-disconnect=0.05", - "-jim-linkspeed-min=1250", - "-jim-linkspeed-max=12500", - }, + Cmd: options, PortBindings: map[docker.Port][]docker.PortBinding{ "8025/tcp": {{HostPort: fmt.Sprintf("%d/tcp", apiPort)}}, "1025/tcp": {{HostPort: fmt.Sprintf("%d/tcp", smtpPort)}}, diff --git a/x/normalize.go b/x/normalize.go new file mode 100644 index 000000000000..9429fa12bd92 --- /dev/null +++ b/x/normalize.go @@ -0,0 +1,77 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "strings" + + "github.com/nyaruka/phonenumbers" + "github.com/pkg/errors" +) + +// NormalizeEmailIdentifier normalizes an email address. +func NormalizeEmailIdentifier(value string) string { + if strings.Contains(value, "@") { + value = strings.TrimSpace(strings.ToLower(value)) + } + return value +} + +// NormalizePhoneIdentifier normalizes a phone number. +func NormalizePhoneIdentifier(value string) string { + if number, err := phonenumbers.Parse(value, ""); err == nil && phonenumbers.IsValidNumber(number) { + value = phonenumbers.Format(number, phonenumbers.E164) + } + return value +} + +// NormalizeOtherIdentifier normalizes an identifier that is not an email or phone number. +func NormalizeOtherIdentifier(value string) string { + return strings.TrimSpace(value) +} + +// GracefulNormalization normalizes an identifier based on the format. +// +// Supported formats are: +// +// - email +// - phone +// - username +func GracefulNormalization(value string) string { + if number, err := phonenumbers.Parse(value, ""); err == nil && phonenumbers.IsValidNumber(number) { + return phonenumbers.Format(number, phonenumbers.E164) + } else if strings.Contains(value, "@") { + return NormalizeEmailIdentifier(value) + } + return NormalizeOtherIdentifier(value) +} + +// NormalizeIdentifier normalizes an identifier based on the format. +// +// Supported formats are: +// +// - email +// - phone +// - username +func NormalizeIdentifier(value, format string) (string, error) { + switch format { + case "email": + return NormalizeEmailIdentifier(value), nil + case "sms": + number, err := phonenumbers.Parse(value, "") + if err != nil { + return "", err + } + + if !phonenumbers.IsValidNumber(number) { + return "", errors.New("the provided number is not a valid phone number") + } + + return phonenumbers.Format(number, phonenumbers.E164), nil + case "username": + fallthrough + default: + return NormalizeOtherIdentifier(value), nil + } +} diff --git a/x/normalize_test.go b/x/normalize_test.go new file mode 100644 index 000000000000..3b7993527c28 --- /dev/null +++ b/x/normalize_test.go @@ -0,0 +1,95 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNormalizeEmailIdentifier(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {" EXAMPLE@DOMAIN.COM ", "example@domain.com"}, + {"user@domain.com", "user@domain.com"}, + {"invalid-email", "invalid-email"}, + } + + for _, test := range tests { + assert.Equal(t, test.expected, NormalizeEmailIdentifier(test.input)) + } +} + +func TestNormalizePhoneIdentifier(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"+1 650-253-0000", "+16502530000"}, + {"+1 (650) 253-0000", "+16502530000"}, + {"invalid-phone", "invalid-phone"}, + } + + for _, test := range tests { + assert.Equal(t, test.expected, NormalizePhoneIdentifier(test.input)) + } +} + +func TestNormalizeOtherIdentifier(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {" username ", "username"}, + {"user123", "user123"}, + {" ", ""}, + } + + for _, test := range tests { + assert.Equal(t, test.expected, NormalizeOtherIdentifier(test.input)) + } +} + +func TestGracefulNormalization(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"+1 650-253-0000", "+16502530000"}, + {" EXAMPLE@DOMAIN.COM ", "example@domain.com"}, + {" username ", "username"}, + {"invalid-phone", "invalid-phone"}, + } + + for _, test := range tests { + assert.Equal(t, test.expected, GracefulNormalization(test.input)) + } +} + +func TestNormalizeIdentifier(t *testing.T) { + tests := []struct { + input string + format string + expected string + err bool + }{ + {" EXAMPLE@DOMAIN.COM ", "email", "example@domain.com", false}, + {"+1 650-253-0000", "sms", "+16502530000", false}, + {" username ", "username", "username", false}, + {"invalid-phone", "sms", "", true}, + } + + for _, test := range tests { + result, err := NormalizeIdentifier(test.input, test.format) + if test.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, test.expected, result) + } + } +} diff --git a/x/redir_test.go b/x/redir_test.go index bbb8417f8b91..1c4a191b429b 100644 --- a/x/redir_test.go +++ b/x/redir_test.go @@ -4,7 +4,6 @@ package x_test import ( - "context" "fmt" "io" "net/http" @@ -12,6 +11,8 @@ import ( "strings" "testing" + "github.com/ory/x/configx" + "github.com/julienschmidt/httprouter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -22,17 +23,16 @@ import ( ) func TestRedirectToPublicAdminRoute(t *testing.T) { - ctx := context.Background() - conf, reg := internal.NewFastRegistryWithMocks(t) pub := x.NewRouterPublic() adm := x.NewRouterAdmin() adminTS := httptest.NewServer(adm) pubTS := httptest.NewServer(pub) t.Cleanup(pubTS.Close) t.Cleanup(adminTS.Close) - - conf.MustSet(ctx, config.ViperKeyAdminBaseURL, adminTS.URL) - conf.MustSet(ctx, config.ViperKeyPublicBaseURL, pubTS.URL) + _, reg := internal.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{ + config.ViperKeyAdminBaseURL: adminTS.URL, + config.ViperKeyPublicBaseURL: pubTS.URL, + })) pub.POST("/privileged", x.RedirectToAdminRoute(reg)) pub.POST("/admin/privileged", x.RedirectToAdminRoute(reg)) diff --git a/x/sql.go b/x/sql.go new file mode 100644 index 000000000000..3c9a1c181f86 --- /dev/null +++ b/x/sql.go @@ -0,0 +1,10 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import "strings" + +func EscapeLikePattern(s string) string { + return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(s, "\\", "\\\\"), "%", "\\%"), "_", "\\_") +} diff --git a/x/sql_test.go b/x/sql_test.go new file mode 100644 index 000000000000..f8c523dc9e17 --- /dev/null +++ b/x/sql_test.go @@ -0,0 +1,34 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEscapeLikePattern(t *testing.T) { + for name, tc := range map[string]struct { + input string + expected string + }{ + "empty": { + input: "", + expected: "", + }, + "no escape": { + input: "foo", + expected: "foo", + }, + "escape": { + input: "foo%bar_baz\\", + expected: "foo\\%bar\\_baz\\\\", + }, + } { + t.Run(name, func(t *testing.T) { + require.Equal(t, tc.expected, EscapeLikePattern(tc.input)) + }) + } +} diff --git a/x/swagger/swagger_types_global.go b/x/swagger/swagger_types_global.go index 2175ff1eca4e..8057ee495dfa 100644 --- a/x/swagger/swagger_types_global.go +++ b/x/swagger/swagger_types_global.go @@ -10,9 +10,6 @@ import "github.com/ory/herodot" // The standard Ory JSON API error format. // // swagger:model errorGeneric -// -//nolint:deadcode,unused -//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions type ErrorGeneric struct { // Contains error details // @@ -23,10 +20,7 @@ type ErrorGeneric struct { // swagger:model genericError type GenericError struct{ herodot.DefaultError } -// Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 201. +// Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 204. // // swagger:response emptyResponse -// -//nolint:deadcode,unused -//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions -type emptyResponse struct{} +type _ struct{} diff --git a/x/transaction.go b/x/transaction.go new file mode 100644 index 000000000000..117a1bf5cb6e --- /dev/null +++ b/x/transaction.go @@ -0,0 +1,20 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "context" + + "github.com/gobuffalo/pop/v6" +) + +type ( + TransactionPersistenceProvider interface { + TransactionalPersisterProvider() TransactionalPersister + } + + TransactionalPersister interface { + Transaction(ctx context.Context, callback func(ctx context.Context, connection *pop.Connection) error) error + } +) diff --git a/x/webauthnx/aaguid/aaguid.go b/x/webauthnx/aaguid/aaguid.go new file mode 100644 index 000000000000..376303848bc9 --- /dev/null +++ b/x/webauthnx/aaguid/aaguid.go @@ -0,0 +1,55 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:generate curl https://raw.githubusercontent.com/passkeydeveloper/passkey-authenticator-aaguids/main/aaguid.json --output passkey-aaguids.json + +package aaguid + +import ( + _ "embed" + "encoding/json" + + "github.com/gofrs/uuid" + "golang.org/x/exp/maps" +) + +var ( + //go:embed aaguids.json + rawAAGUIDs []byte + //go:embed passkey-aaguids.json + rawPasskeyAAGUIDs []byte + aaguids map[string]AAGUID +) + +type AAGUID struct { + Name string `json:"name"` + IconDark string `json:"icon_dark"` + IconLight string `json:"icon_light"` +} + +func init() { + err := json.Unmarshal(rawAAGUIDs, &aaguids) + if err != nil { + panic(err) + } + + var passkeyAAGUIDs map[string]AAGUID + err = json.Unmarshal(rawPasskeyAAGUIDs, &passkeyAAGUIDs) + if err != nil { + panic(err) + } + + maps.Copy(aaguids, passkeyAAGUIDs) +} + +func Lookup(id []byte) *AAGUID { + uid, err := uuid.FromBytes(id) + if err != nil { + return nil + } + + if aaguid, ok := aaguids[uid.String()]; ok { + return &aaguid + } + return nil +} diff --git a/x/webauthnx/aaguid/aaguid_test.go b/x/webauthnx/aaguid/aaguid_test.go new file mode 100644 index 000000000000..8f7d83a8b315 --- /dev/null +++ b/x/webauthnx/aaguid/aaguid_test.go @@ -0,0 +1,51 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package aaguid + +import ( + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" +) + +func TestLookup(t *testing.T) { + goodUUIDBytes := uuid.Must(uuid.FromString("adce0002-35bc-c60a-648b-0b25f1f05503")).Bytes() + badUUIDBytes := uuid.Must(uuid.NewV4()).Bytes() + + tests := []struct { + name string + id []byte + want string + }{ + { + name: "GoodUUID", + id: goodUUIDBytes, + want: "Chrome on Mac", + }, + { + name: "BadUUID", + id: badUUIDBytes, + }, + { + name: "NilUUID", + id: nil, + }, + { + name: "EmptyUUID", + id: []byte{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := Lookup(tt.id) + if tt.want == "" { + assert.Nil(t, res) + } else { + assert.Equal(t, tt.want, res.Name) + } + }) + } +} diff --git a/x/webauthnx/aaguid/aaguids.json b/x/webauthnx/aaguid/aaguids.json new file mode 100644 index 000000000000..85fa3dcd20b0 --- /dev/null +++ b/x/webauthnx/aaguid/aaguids.json @@ -0,0 +1,603 @@ +{ + "ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4": { + "name": "Google Password Manager", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE5MiAxOTIiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDE5MiAxOTIiIHdpZHRoPSIyNHB4Ij48cmVjdCBmaWxsPSJub25lIiBoZWlnaHQ9IjE5MiIgd2lkdGg9IjE5MiIgeT0iMCIvPjxnPjxwYXRoIGQ9Ik02OS4yOSwxMDZjLTMuNDYsNS45Ny05LjkxLDEwLTE3LjI5LDEwYy0xMS4wMywwLTIwLTguOTctMjAtMjBzOC45Ny0yMCwyMC0yMCBjNy4zOCwwLDEzLjgzLDQuMDMsMTcuMjksMTBoMjUuNTVDOTAuMyw2Ni41NCw3Mi44Miw1Miw1Miw1MkMyNy43NCw1Miw4LDcxLjc0LDgsOTZzMTkuNzQsNDQsNDQsNDRjMjAuODIsMCwzOC4zLTE0LjU0LDQyLjg0LTM0IEg2OS4yOXoiIGZpbGw9IiM0Mjg1RjQiLz48cmVjdCBmaWxsPSIjRkJCQzA0IiBoZWlnaHQ9IjI0IiB3aWR0aD0iNDQiIHg9Ijk0IiB5PSI4NCIvPjxwYXRoIGQ9Ik05NC4zMiw4NEg2OHYwLjA1YzIuNSwzLjM0LDQsNy40Nyw0LDExLjk1cy0xLjUsOC42MS00LDExLjk1VjEwOGgyNi4zMiBjMS4wOC0zLjgyLDEuNjgtNy44NCwxLjY4LTEyUzk1LjQxLDg3LjgyLDk0LjMyLDg0eiIgZmlsbD0iI0VBNDMzNSIvPjxwYXRoIGQ9Ik0xODQsMTA2djI2aC0xNnYtOGMwLTQuNDItMy41OC04LTgtOHMtOCwzLjU4LTgsOHY4aC0xNnYtMjZIMTg0eiIgZmlsbD0iIzM0QTg1MyIvPjxyZWN0IGZpbGw9IiMxODgwMzgiIGhlaWdodD0iMjQiIHdpZHRoPSI0OCIgeD0iMTM2IiB5PSI4NCIvPjwvZz48L3N2Zz4=", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE5MiAxOTIiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDE5MiAxOTIiIHdpZHRoPSIyNHB4Ij48cmVjdCBmaWxsPSJub25lIiBoZWlnaHQ9IjE5MiIgd2lkdGg9IjE5MiIgeT0iMCIvPjxnPjxwYXRoIGQ9Ik02OS4yOSwxMDZjLTMuNDYsNS45Ny05LjkxLDEwLTE3LjI5LDEwYy0xMS4wMywwLTIwLTguOTctMjAtMjBzOC45Ny0yMCwyMC0yMCBjNy4zOCwwLDEzLjgzLDQuMDMsMTcuMjksMTBoMjUuNTVDOTAuMyw2Ni41NCw3Mi44Miw1Miw1Miw1MkMyNy43NCw1Miw4LDcxLjc0LDgsOTZzMTkuNzQsNDQsNDQsNDRjMjAuODIsMCwzOC4zLTE0LjU0LDQyLjg0LTM0IEg2OS4yOXoiIGZpbGw9IiM0Mjg1RjQiLz48cmVjdCBmaWxsPSIjRkJCQzA0IiBoZWlnaHQ9IjI0IiB3aWR0aD0iNDQiIHg9Ijk0IiB5PSI4NCIvPjxwYXRoIGQ9Ik05NC4zMiw4NEg2OHYwLjA1YzIuNSwzLjM0LDQsNy40Nyw0LDExLjk1cy0xLjUsOC42MS00LDExLjk1VjEwOGgyNi4zMiBjMS4wOC0zLjgyLDEuNjgtNy44NCwxLjY4LTEyUzk1LjQxLDg3LjgyLDk0LjMyLDg0eiIgZmlsbD0iI0VBNDMzNSIvPjxwYXRoIGQ9Ik0xODQsMTA2djI2aC0xNnYtOGMwLTQuNDItMy41OC04LTgtOHMtOCwzLjU4LTgsOHY4aC0xNnYtMjZIMTg0eiIgZmlsbD0iIzM0QTg1MyIvPjxyZWN0IGZpbGw9IiMxODgwMzgiIGhlaWdodD0iMjQiIHdpZHRoPSI0OCIgeD0iMTM2IiB5PSI4NCIvPjwvZz48L3N2Zz4=" + }, + "adce0002-35bc-c60a-648b-0b25f1f05503": { + "name": "Chrome on Mac", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDggNDgiPgogIDxkZWZzPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJhIiB4MT0iMy4yMTczIiB5MT0iMTUiIHgyPSI0NC43ODEyIiB5Mj0iMTUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZDkzMDI1Ii8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2VhNDMzNSIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjIwLjcyMTkiIHkxPSI0Ny42NzkxIiB4Mj0iNDEuNTAzOSIgeTI9IjExLjY4MzciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmNjOTM0Ii8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZiYmMwNCIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYyIgeDE9IjI2LjU5ODEiIHkxPSI0Ni41MDE1IiB4Mj0iNS44MTYxIiB5Mj0iMTAuNTA2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzFlOGUzZSIvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzNGE4NTMiLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgICAKICAgIDxwYXRoIGlkPSJwIiBkPSJNMTMuNjA4NiAzMC4wMDMxIDMuMjE4IDEyLjAwNkEyMy45OTQgMjMuOTk0IDAgMCAwIDI0LjAwMjUgNDhsMTAuMzkwNi0xNy45OTcxLS4wMDY3LS4wMDY4YTExLjk4NTIgMTEuOTg1MiAwIDAgMS0yMC43Nzc4LjAwN1oiLz4KICA8L2RlZnM+CiAgCiAgPHVzZSB4bGluazpocmVmPSIjcCIgZmlsbD0idXJsKCNhKSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDI0IDI0KSIvPgogIDx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGw9InVybCgjYikiIHRyYW5zZm9ybT0icm90YXRlKC0xMjAgMjQgMjQpIi8+CiAgPHVzZSB4bGluazpocmVmPSIjcCIgZmlsbD0idXJsKCNjKSIvPgogIAogIDxjaXJjbGUgY3g9IjI0IiBjeT0iMjQiIHI9IjEyIiBzdHlsZT0iZmlsbDojZmZmIi8+CiAgPGNpcmNsZSBjeD0iMjQiIGN5PSIyNCIgcj0iOS41IiBzdHlsZT0iZmlsbDojMWE3M2U4Ii8+Cjwvc3ZnPg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDggNDgiPgogIDxkZWZzPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJhIiB4MT0iMy4yMTczIiB5MT0iMTUiIHgyPSI0NC43ODEyIiB5Mj0iMTUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZDkzMDI1Ii8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2VhNDMzNSIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjIwLjcyMTkiIHkxPSI0Ny42NzkxIiB4Mj0iNDEuNTAzOSIgeTI9IjExLjY4MzciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmNjOTM0Ii8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZiYmMwNCIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYyIgeDE9IjI2LjU5ODEiIHkxPSI0Ni41MDE1IiB4Mj0iNS44MTYxIiB5Mj0iMTAuNTA2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzFlOGUzZSIvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzNGE4NTMiLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgICAKICAgIDxwYXRoIGlkPSJwIiBkPSJNMTMuNjA4NiAzMC4wMDMxIDMuMjE4IDEyLjAwNkEyMy45OTQgMjMuOTk0IDAgMCAwIDI0LjAwMjUgNDhsMTAuMzkwNi0xNy45OTcxLS4wMDY3LS4wMDY4YTExLjk4NTIgMTEuOTg1MiAwIDAgMS0yMC43Nzc4LjAwN1oiLz4KICA8L2RlZnM+CiAgCiAgPHVzZSB4bGluazpocmVmPSIjcCIgZmlsbD0idXJsKCNhKSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDI0IDI0KSIvPgogIDx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGw9InVybCgjYikiIHRyYW5zZm9ybT0icm90YXRlKC0xMjAgMjQgMjQpIi8+CiAgPHVzZSB4bGluazpocmVmPSIjcCIgZmlsbD0idXJsKCNjKSIvPgogIAogIDxjaXJjbGUgY3g9IjI0IiBjeT0iMjQiIHI9IjEyIiBzdHlsZT0iZmlsbDojZmZmIi8+CiAgPGNpcmNsZSBjeD0iMjQiIGN5PSIyNCIgcj0iOS41IiBzdHlsZT0iZmlsbDojMWE3M2U4Ii8+Cjwvc3ZnPg==" + }, + "08987058-cadc-4b81-b6e1-30de50dcbe96": { + "name": "Windows Hello Hardware Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACkUlEQVR42uyai3GDMAyGQyegGzACnaCMkBHoBhkhnSAj0A2SDaAT0E6QbEA3cOXW6XEpBtnImMv9utOllxjF/qKHLTdRSm0gdnkAAgACIAACIAACIAACIAgAARAAARAAARAAARBEAFCSJINKkpLuSTtSZbQz76W25zhKkpFWPbtaz6Q75vPuoluuPmqxlZK2yi76s9RznjlpN2K7CrFWaUAHNS0HT0Atw3YpDSjxbdoPuaziG3uk579cvIdeWsbQD7L7NAYoWpKmLy8chueO5reB7KKKrQnQJdDYn9AJZHc5QBT7enINY2hjxrqItsvJWSdxFxKuYlOlWJmE6zPPcsJuN7WFiF7me5DOAws4OyZyG6TOsr/KQziDaJm/mcy2V1V0+T0JeXxqqlrWC9mGGy3O6wwFaI0SdR+EMg9AEAACIAByqViZb+/prgFdN6qb306j3lTWs0BJ76Qjw0ktO+3ad60PQhMrfM9YwqK7lUPe4j+/OR40cDaqJeJ+xo80JsWih1WTBAcb8ysKrb+TfowQKy3v55wbBkk49FJbQusqr4snadL9hEtXC3nO1G1HG6UfxIj5oDnJlHPOVVAerWGmvYQxwc70hiTh7Bidy3/3ZFE6isxf8epNhUCl4n5ftYqWKzMP3IIquaFnquXO0sZ1yn/RWq69SuK6GdPXORfSz4HPnk1bNXO0+UZze5HqKIodNYwnHVVcOUivNcStxj4CGFYhWAWgXgmuF4JzdMhn6wDUm1DpmFyVY7IvQqeTRdod2v2F8lNn/gcpW+rUsOi9mAmFwlSo3Pw9JQ3p+8bhgnAMkPM613BxOBQqc2FEB4SmPQSAAAiAAAiAAAiAAAiAIAAEQAAEQAAEQPco3wIMADOXgFhOTghuAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACkUlEQVR42uyai3GDMAyGQyegGzACnaCMkBHoBhkhnSAj0A2SDaAT0E6QbEA3cOXW6XEpBtnImMv9utOllxjF/qKHLTdRSm0gdnkAAgACIAACIAACIAACIAgAARAAARAAARAAARBEAFCSJINKkpLuSTtSZbQz76W25zhKkpFWPbtaz6Q75vPuoluuPmqxlZK2yi76s9RznjlpN2K7CrFWaUAHNS0HT0Atw3YpDSjxbdoPuaziG3uk579cvIdeWsbQD7L7NAYoWpKmLy8chueO5reB7KKKrQnQJdDYn9AJZHc5QBT7enINY2hjxrqItsvJWSdxFxKuYlOlWJmE6zPPcsJuN7WFiF7me5DOAws4OyZyG6TOsr/KQziDaJm/mcy2V1V0+T0JeXxqqlrWC9mGGy3O6wwFaI0SdR+EMg9AEAACIAByqViZb+/prgFdN6qb306j3lTWs0BJ76Qjw0ktO+3ad60PQhMrfM9YwqK7lUPe4j+/OR40cDaqJeJ+xo80JsWih1WTBAcb8ysKrb+TfowQKy3v55wbBkk49FJbQusqr4snadL9hEtXC3nO1G1HG6UfxIj5oDnJlHPOVVAerWGmvYQxwc70hiTh7Bidy3/3ZFE6isxf8epNhUCl4n5ftYqWKzMP3IIquaFnquXO0sZ1yn/RWq69SuK6GdPXORfSz4HPnk1bNXO0+UZze5HqKIodNYwnHVVcOUivNcStxj4CGFYhWAWgXgmuF4JzdMhn6wDUm1DpmFyVY7IvQqeTRdod2v2F8lNn/gcpW+rUsOi9mAmFwlSo3Pw9JQ3p+8bhgnAMkPM613BxOBQqc2FEB4SmPQSAAAiAAAiAAAiAAAiAIAAEQAAEQAAEQPco3wIMADOXgFhOTghuAAAAAElFTkSuQmCC" + }, + "9ddd1817-af5a-4672-a2b9-3e3dd95000a9": { + "name": "Windows Hello VBS Hardware Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACkUlEQVR42uyai3GDMAyGQyegGzACnaCMkBHoBhkhnSAj0A2SDaAT0E6QbEA3cOXW6XEpBtnImMv9utOllxjF/qKHLTdRSm0gdnkAAgACIAACIAACIAACIAgAARAAARAAARAAARBEAFCSJINKkpLuSTtSZbQz76W25zhKkpFWPbtaz6Q75vPuoluuPmqxlZK2yi76s9RznjlpN2K7CrFWaUAHNS0HT0Atw3YpDSjxbdoPuaziG3uk579cvIdeWsbQD7L7NAYoWpKmLy8chueO5reB7KKKrQnQJdDYn9AJZHc5QBT7enINY2hjxrqItsvJWSdxFxKuYlOlWJmE6zPPcsJuN7WFiF7me5DOAws4OyZyG6TOsr/KQziDaJm/mcy2V1V0+T0JeXxqqlrWC9mGGy3O6wwFaI0SdR+EMg9AEAACIAByqViZb+/prgFdN6qb306j3lTWs0BJ76Qjw0ktO+3ad60PQhMrfM9YwqK7lUPe4j+/OR40cDaqJeJ+xo80JsWih1WTBAcb8ysKrb+TfowQKy3v55wbBkk49FJbQusqr4snadL9hEtXC3nO1G1HG6UfxIj5oDnJlHPOVVAerWGmvYQxwc70hiTh7Bidy3/3ZFE6isxf8epNhUCl4n5ftYqWKzMP3IIquaFnquXO0sZ1yn/RWq69SuK6GdPXORfSz4HPnk1bNXO0+UZze5HqKIodNYwnHVVcOUivNcStxj4CGFYhWAWgXgmuF4JzdMhn6wDUm1DpmFyVY7IvQqeTRdod2v2F8lNn/gcpW+rUsOi9mAmFwlSo3Pw9JQ3p+8bhgnAMkPM613BxOBQqc2FEB4SmPQSAAAiAAAiAAAiAAAiAIAAEQAAEQAAEQPco3wIMADOXgFhOTghuAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACkUlEQVR42uyai3GDMAyGQyegGzACnaCMkBHoBhkhnSAj0A2SDaAT0E6QbEA3cOXW6XEpBtnImMv9utOllxjF/qKHLTdRSm0gdnkAAgACIAACIAACIAACIAgAARAAARAAARAAARBEAFCSJINKkpLuSTtSZbQz76W25zhKkpFWPbtaz6Q75vPuoluuPmqxlZK2yi76s9RznjlpN2K7CrFWaUAHNS0HT0Atw3YpDSjxbdoPuaziG3uk579cvIdeWsbQD7L7NAYoWpKmLy8chueO5reB7KKKrQnQJdDYn9AJZHc5QBT7enINY2hjxrqItsvJWSdxFxKuYlOlWJmE6zPPcsJuN7WFiF7me5DOAws4OyZyG6TOsr/KQziDaJm/mcy2V1V0+T0JeXxqqlrWC9mGGy3O6wwFaI0SdR+EMg9AEAACIAByqViZb+/prgFdN6qb306j3lTWs0BJ76Qjw0ktO+3ad60PQhMrfM9YwqK7lUPe4j+/OR40cDaqJeJ+xo80JsWih1WTBAcb8ysKrb+TfowQKy3v55wbBkk49FJbQusqr4snadL9hEtXC3nO1G1HG6UfxIj5oDnJlHPOVVAerWGmvYQxwc70hiTh7Bidy3/3ZFE6isxf8epNhUCl4n5ftYqWKzMP3IIquaFnquXO0sZ1yn/RWq69SuK6GdPXORfSz4HPnk1bNXO0+UZze5HqKIodNYwnHVVcOUivNcStxj4CGFYhWAWgXgmuF4JzdMhn6wDUm1DpmFyVY7IvQqeTRdod2v2F8lNn/gcpW+rUsOi9mAmFwlSo3Pw9JQ3p+8bhgnAMkPM613BxOBQqc2FEB4SmPQSAAAiAAAiAAAiAAAiAIAAEQAAEQAAEQPco3wIMADOXgFhOTghuAAAAAElFTkSuQmCC" + }, + "6028b017-b1d4-4c02-b4b3-afcdafc96bb2": { + "name": "Windows Hello Software Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACkUlEQVR42uyai3GDMAyGQyegGzACnaCMkBHoBhkhnSAj0A2SDaAT0E6QbEA3cOXW6XEpBtnImMv9utOllxjF/qKHLTdRSm0gdnkAAgACIAACIAACIAACIAgAARAAARAAARAAARBEAFCSJINKkpLuSTtSZbQz76W25zhKkpFWPbtaz6Q75vPuoluuPmqxlZK2yi76s9RznjlpN2K7CrFWaUAHNS0HT0Atw3YpDSjxbdoPuaziG3uk579cvIdeWsbQD7L7NAYoWpKmLy8chueO5reB7KKKrQnQJdDYn9AJZHc5QBT7enINY2hjxrqItsvJWSdxFxKuYlOlWJmE6zPPcsJuN7WFiF7me5DOAws4OyZyG6TOsr/KQziDaJm/mcy2V1V0+T0JeXxqqlrWC9mGGy3O6wwFaI0SdR+EMg9AEAACIAByqViZb+/prgFdN6qb306j3lTWs0BJ76Qjw0ktO+3ad60PQhMrfM9YwqK7lUPe4j+/OR40cDaqJeJ+xo80JsWih1WTBAcb8ysKrb+TfowQKy3v55wbBkk49FJbQusqr4snadL9hEtXC3nO1G1HG6UfxIj5oDnJlHPOVVAerWGmvYQxwc70hiTh7Bidy3/3ZFE6isxf8epNhUCl4n5ftYqWKzMP3IIquaFnquXO0sZ1yn/RWq69SuK6GdPXORfSz4HPnk1bNXO0+UZze5HqKIodNYwnHVVcOUivNcStxj4CGFYhWAWgXgmuF4JzdMhn6wDUm1DpmFyVY7IvQqeTRdod2v2F8lNn/gcpW+rUsOi9mAmFwlSo3Pw9JQ3p+8bhgnAMkPM613BxOBQqc2FEB4SmPQSAAAiAAAiAAAiAAAiAIAAEQAAEQAAEQPco3wIMADOXgFhOTghuAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACkUlEQVR42uyai3GDMAyGQyegGzACnaCMkBHoBhkhnSAj0A2SDaAT0E6QbEA3cOXW6XEpBtnImMv9utOllxjF/qKHLTdRSm0gdnkAAgACIAACIAACIAACIAgAARAAARAAARAAARBEAFCSJINKkpLuSTtSZbQz76W25zhKkpFWPbtaz6Q75vPuoluuPmqxlZK2yi76s9RznjlpN2K7CrFWaUAHNS0HT0Atw3YpDSjxbdoPuaziG3uk579cvIdeWsbQD7L7NAYoWpKmLy8chueO5reB7KKKrQnQJdDYn9AJZHc5QBT7enINY2hjxrqItsvJWSdxFxKuYlOlWJmE6zPPcsJuN7WFiF7me5DOAws4OyZyG6TOsr/KQziDaJm/mcy2V1V0+T0JeXxqqlrWC9mGGy3O6wwFaI0SdR+EMg9AEAACIAByqViZb+/prgFdN6qb306j3lTWs0BJ76Qjw0ktO+3ad60PQhMrfM9YwqK7lUPe4j+/OR40cDaqJeJ+xo80JsWih1WTBAcb8ysKrb+TfowQKy3v55wbBkk49FJbQusqr4snadL9hEtXC3nO1G1HG6UfxIj5oDnJlHPOVVAerWGmvYQxwc70hiTh7Bidy3/3ZFE6isxf8epNhUCl4n5ftYqWKzMP3IIquaFnquXO0sZ1yn/RWq69SuK6GdPXORfSz4HPnk1bNXO0+UZze5HqKIodNYwnHVVcOUivNcStxj4CGFYhWAWgXgmuF4JzdMhn6wDUm1DpmFyVY7IvQqeTRdod2v2F8lNn/gcpW+rUsOi9mAmFwlSo3Pw9JQ3p+8bhgnAMkPM613BxOBQqc2FEB4SmPQSAAAiAAAiAAAiAAAiAIAAEQAAEQAAEQPco3wIMADOXgFhOTghuAAAAAElFTkSuQmCC" + }, + "dd4ec289-e01d-41c9-bb89-70fa845d4bf2": { + "name": "Apple iCloud Keychain (Managed)" + }, + "531126d6-e717-415c-9320-3d9aa6981239": { + "name": "Dashlane", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yNTcuNDc0IDM1OS4yMDlDMjU3LjQ3NCAzNTYuMTg5IDI1NC40NTQgMzUzLjE2OSAyNTAuMjE1IDM1MS45NTlMMTk5LjQxMSAzMzMuMjMxQzE5MC44OTUgMzI5LjYwMSAxODEuMjY0IDMzMy44MzEgMTgxLjI2NCAzMzkuODlWNDc1Ljc3OUMxODEuMjY0IDQ3OC44MDkgMTg0LjI4MyA0ODIuNDM4IDE4Ny4zMDMgNDgzLjY0OEwyMzkuMzI2IDUwMi4zNzZDMjQ3LjE5NSA1MDUuMzk2IDI1Ny40NzQgNTAxLjE2NiAyNTcuNDc0IDQ5NC41MDhWMzU5LjIwOVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0zNTIuNzM2IDMyMS4xMDRDMzUyLjczNiAzMTguMDg0IDM0OS43MTcgMzE1LjA2NCAzNDUuNDc3IDMxMy44NTRMMjk0LjY3NCAyOTUuMTI2QzI4Ni4xNTcgMjkxLjQ5NiAyNzYuNTI2IDI5NS43MjYgMjc2LjUyNiAzMDEuNzg1VjQzNy42NzRDMjc2LjUyNiA0NDAuNzA0IDI3OS41NDYgNDQ0LjMzMyAyODIuNTY2IDQ0NS41NDNMMzM0LjU4OSA0NjQuMjcxQzM0Mi40NTggNDY3LjI5MSAzNTIuNzM2IDQ2My4wNjEgMzUyLjczNiA0NTYuNDAzVjMyMS4xMDRaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjU3LjQ3NCAzNS4zMjExQzI1Ny40NzQgMzIuMzAxMyAyNTQuNDU0IDI5LjI4MTUgMjUwLjIxNSAyOC4wNzE3TDE5OS40MTEgOS4zNDM0M0MxOTAuODk1IDUuNzEzOTkgMTgxLjI2NCA5Ljk0MzU4IDE4MS4yNjQgMTYuMDAyMlYxNTEuODkyQzE4MS4yNjQgMTU0LjkyMSAxODQuMjgzIDE1OC41NTEgMTg3LjMwMyAxNTkuNzZMMjM5LjMyNiAxNzguNDg5QzI0Ny4xOTUgMTgxLjUwOSAyNTcuNDc0IDE3Ny4yNzkgMjU3LjQ3NCAxNzAuNjJWMzUuMzIxMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0zNTIuNzM2IDkyLjQ3NzdDMzUyLjczNiA4OS40NTc5IDM0OS43MTcgODYuNDM4MiAzNDUuNDc3IDg1LjIyODNMMjk0LjY3NCA2Ni41QzI4Ni4xNTcgNjIuODcwNiAyNzYuNTI2IDY3LjEwMDIgMjc2LjUyNiA3My4xNTg4VjIwOS4wNDhDMjc2LjUyNiAyMTIuMDc4IDI3OS41NDYgMjE1LjcwNyAyODIuNTY2IDIxNi45MTdMMzM0LjU4OSAyMzUuNjQ1QzM0Mi40NTggMjM4LjY2NSAzNTIuNzM2IDIzNC40MzYgMzUyLjczNiAyMjcuNzc3VjkyLjQ3NzdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNDQ4IDE2OC42ODdDNDQ4IDE2NS42NjcgNDQ0Ljk4IDE2Mi42NDcgNDQwLjc0MSAxNjEuNDM3TDM4OS45MzcgMTQyLjcwOUMzODEuNDIxIDEzOS4wNzkgMzcxLjc5IDE0My4zMDkgMzcxLjc5IDE0OS4zNjhWMzYxLjQ2NkMzNzEuNzkgMzY0LjQ5NSAzNzQuODEgMzY4LjEyNSAzNzcuODI5IDM2OS4zMzVMNDI5Ljg1MiAzODguMDYzQzQzNy43MjEgMzkxLjA4MyA0NDggMzg2Ljg1MyA0NDggMzgwLjE5NFYxNjguNjg3WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTE2Mi4yMSAzNS4zMzA2QzE2Mi4yMSAzMi4zMTA4IDE1OS4xOSAyOS4yODE1IDE1NC45NTEgMjguMDcxN0wxMDQuMTQ4IDkuMzQzNDNDOTUuNjc4NyA1LjcxMzk5IDg2IDkuOTQzNTggODYgMTYuMDAyMlY0NzUuNzg5Qzg2IDQ3OC44MDggODkuMDE5OCA0ODIuNDM4IDkyLjA0OTIgNDgzLjY0OEwxNDQuMDYzIDUwMi4zNzZDMTUxLjkzMSA1MDUuMzk2IDE2Mi4yMSA1MDEuMTY2IDE2Mi4yMSA0OTQuNTA3VjM1LjMzMDZaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yNTcuNDc0IDM1OS4yMDlDMjU3LjQ3NCAzNTYuMTg5IDI1NC40NTQgMzUzLjE2OSAyNTAuMjE1IDM1MS45NTlMMTk5LjQxMSAzMzMuMjMxQzE5MC44OTUgMzI5LjYwMSAxODEuMjY0IDMzMy44MzEgMTgxLjI2NCAzMzkuODlWNDc1Ljc3OUMxODEuMjY0IDQ3OC44MDkgMTg0LjI4MyA0ODIuNDM4IDE4Ny4zMDMgNDgzLjY0OEwyMzkuMzI2IDUwMi4zNzZDMjQ3LjE5NSA1MDUuMzk2IDI1Ny40NzQgNTAxLjE2NiAyNTcuNDc0IDQ5NC41MDhWMzU5LjIwOVoiIGZpbGw9IiMwOTM2M0YiLz4KPHBhdGggZD0iTTM1Mi43MzYgMzIxLjEwM0MzNTIuNzM2IDMxOC4wODQgMzQ5LjcxNyAzMTUuMDY0IDM0NS40NzcgMzEzLjg1NEwyOTQuNjc0IDI5NS4xMjZDMjg2LjE1NyAyOTEuNDk2IDI3Ni41MjYgMjk1LjcyNiAyNzYuNTI2IDMwMS43ODVWNDM3LjY3NEMyNzYuNTI2IDQ0MC43MDQgMjc5LjU0NiA0NDQuMzMzIDI4Mi41NjYgNDQ1LjU0M0wzMzQuNTg5IDQ2NC4yNzFDMzQyLjQ1OCA0NjcuMjkxIDM1Mi43MzYgNDYzLjA2MSAzNTIuNzM2IDQ1Ni40MDNWMzIxLjEwM1oiIGZpbGw9IiMwOTM2M0YiLz4KPHBhdGggZD0iTTI1Ny40NzQgMzUuMzIxMUMyNTcuNDc0IDMyLjMwMTMgMjU0LjQ1NCAyOS4yODE1IDI1MC4yMTUgMjguMDcxN0wxOTkuNDExIDkuMzQzNDNDMTkwLjg5NSA1LjcxMzk5IDE4MS4yNjQgOS45NDM1OCAxODEuMjY0IDE2LjAwMjJWMTUxLjg5MkMxODEuMjY0IDE1NC45MjEgMTg0LjI4MyAxNTguNTUxIDE4Ny4zMDMgMTU5Ljc2TDIzOS4zMjYgMTc4LjQ4OUMyNDcuMTk1IDE4MS41MDggMjU3LjQ3NCAxNzcuMjc5IDI1Ny40NzQgMTcwLjYyVjM1LjMyMTFaIiBmaWxsPSIjMDkzNjNGIi8+CjxwYXRoIGQ9Ik0zNTIuNzM2IDkyLjQ3NzdDMzUyLjczNiA4OS40NTc5IDM0OS43MTcgODYuNDM4MiAzNDUuNDc3IDg1LjIyODNMMjk0LjY3NCA2Ni41QzI4Ni4xNTcgNjIuODcwNiAyNzYuNTI2IDY3LjEwMDIgMjc2LjUyNiA3My4xNTg4VjIwOS4wNDhDMjc2LjUyNiAyMTIuMDc4IDI3OS41NDYgMjE1LjcwNyAyODIuNTY2IDIxNi45MTdMMzM0LjU4OSAyMzUuNjQ1QzM0Mi40NTggMjM4LjY2NSAzNTIuNzM2IDIzNC40MzYgMzUyLjczNiAyMjcuNzc3VjkyLjQ3NzdaIiBmaWxsPSIjMDkzNjNGIi8+CjxwYXRoIGQ9Ik00NDggMTY4LjY4N0M0NDggMTY1LjY2NyA0NDQuOTggMTYyLjY0NyA0NDAuNzQxIDE2MS40MzdMMzg5LjkzNyAxNDIuNzA5QzM4MS40MjEgMTM5LjA3OSAzNzEuNzkgMTQzLjMwOSAzNzEuNzkgMTQ5LjM2OFYzNjEuNDY2QzM3MS43OSAzNjQuNDk1IDM3NC44MSAzNjguMTI1IDM3Ny44MjkgMzY5LjMzNUw0MjkuODUyIDM4OC4wNjNDNDM3LjcyMSAzOTEuMDgzIDQ0OCAzODYuODUzIDQ0OCAzODAuMTk0VjE2OC42ODdaIiBmaWxsPSIjMDkzNjNGIi8+CjxwYXRoIGQ9Ik0xNjIuMjEgMzUuMzMwNkMxNjIuMjEgMzIuMzEwOCAxNTkuMTkgMjkuMjgxNSAxNTQuOTUxIDI4LjA3MTdMMTA0LjE0OCA5LjM0MzQzQzk1LjY3ODcgNS43MTM5OSA4NiA5Ljk0MzU4IDg2IDE2LjAwMjJWNDc1Ljc4OUM4NiA0NzguODA4IDg5LjAxOTggNDgyLjQzOCA5Mi4wNDkyIDQ4My42NDhMMTQ0LjA2MyA1MDIuMzc2QzE1MS45MzEgNTA1LjM5NiAxNjIuMjEgNTAxLjE2NiAxNjIuMjEgNDk0LjUwN1YzNS4zMzA2WiIgZmlsbD0iIzA5MzYzRiIvPgo8L3N2Zz4K" + }, + "bada5566-a7aa-401f-bd96-45619a55120d": { + "name": "1Password", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQwIiBoZWlnaHQ9IjI0MCIgdmlld0JveD0iMCAwIDI0MCAyNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMjM5LjI1NyAxMjAuNDE3QzIzOS4yNTcgNTQuNDE5MyAxODUuNzU1IDAuOTE2NTA0IDExOS43NTcgMC45MTY1MDRDNTMuNzYwMSAwLjkxNjUwNCAwLjI1NzMyNCA1NC40MTkzIDAuMjU3MzI0IDEyMC40MTdDMC4yNTczMjQgMTg2LjQxNyA1My43NjAxIDIzOS45MTcgMTE5Ljc1NyAyMzkuOTE3QzE4NS43NTUgMjM5LjkxNyAyMzkuMjU3IDE4Ni40MTcgMjM5LjI1NyAxMjAuNDE3Wk05OC4wMDY5IDU0LjAyNzZDOTcuMDY3NCA1NS44NzE0IDk3LjA2NzQgNTguMjg1MSA5Ny4wNjc0IDYzLjExMjZWOTAuNDcyOUM5Ny4wNjc0IDkxLjY3ODggOTcuMDY3NCA5Mi4yODE3IDk3LjIxOTYgOTIuODM5MkM5Ny4zNTQ1IDkzLjMzMzEgOTcuNTc2MyA5My43OTkgOTcuODc0NiA5NC4yMTVDOTguMjExMyA5NC42ODQ3IDk4LjY3OTIgOTUuMDY0OCA5OS42MTUyIDk1LjgyNTFMMTA2LjUzNiAxMDEuNDQ3QzEwNy42NjQgMTAyLjM2NCAxMDguMjI4IDEwMi44MjIgMTA4LjQzMyAxMDMuMzc0QzEwOC42MTMgMTAzLjg1NyAxMDguNjEzIDEwNC4zOSAxMDguNDMzIDEwNC44NzNDMTA4LjIyOCAxMDUuNDI1IDEwNy42NjQgMTA1Ljg4MyAxMDYuNTM2IDEwNi44TDk5LjYxNTIgMTEyLjQyMkM5OC42NzkzIDExMy4xODIgOTguMjExMyAxMTMuNTYyIDk3Ljg3NDYgMTE0LjAzMkM5Ny41NzYzIDExNC40NDggOTcuMzU0NSAxMTQuOTE0IDk3LjIxOTYgMTE1LjQwOEM5Ny4wNjc0IDExNS45NjUgOTcuMDY3NCAxMTYuNTY4IDk3LjA2NzQgMTE3Ljc3NFYxNzcuNzE5Qzk3LjA2NzQgMTgyLjU0NyA5Ny4wNjc0IDE4NC45NjEgOTguMDA2OSAxODYuODA1Qzk4LjgzMzMgMTg4LjQyNiAxMDAuMTUyIDE4OS43NDUgMTAxLjc3NCAxOTAuNTcxQzEwMy42MTggMTkxLjUxMSAxMDYuMDMxIDE5MS41MTEgMTEwLjg1OSAxOTEuNTExSDEyOC42NTZDMTMzLjQ4MyAxOTEuNTExIDEzNS44OTcgMTkxLjUxMSAxMzcuNzQxIDE5MC41NzFDMTM5LjM2MyAxODkuNzQ1IDE0MC42ODEgMTg4LjQyNiAxNDEuNTA4IDE4Ni44MDVDMTQyLjQ0NyAxODQuOTYxIDE0Mi40NDcgMTgyLjU0NyAxNDIuNDQ3IDE3Ny43MTlWMTUwLjM1OUMxNDIuNDQ3IDE0OS4xNTMgMTQyLjQ0NyAxNDguNTUgMTQyLjI5NSAxNDcuOTkzQzE0Mi4xNiAxNDcuNDk5IDE0MS45MzggMTQ3LjAzMyAxNDEuNjQgMTQ2LjYxN0MxNDEuMzAzIDE0Ni4xNDcgMTQwLjgzNSAxNDUuNzY3IDEzOS44OTkgMTQ1LjAwN0wxMzIuOTc4IDEzOS4zODVDMTMxLjg1IDEzOC40NjggMTMxLjI4NiAxMzguMDEgMTMxLjA4MiAxMzcuNDU5QzEzMC45MDIgMTM2Ljk3NSAxMzAuOTAyIDEzNi40NDMgMTMxLjA4MiAxMzUuOTU5QzEzMS4yODYgMTM1LjQwNyAxMzEuODUgMTM0Ljk0OSAxMzIuOTc4IDEzNC4wMzNMMTM5Ljg5OSAxMjguNDFDMTQwLjgzNSAxMjcuNjUgMTQxLjMwMyAxMjcuMjcgMTQxLjY0IDEyNi44QzE0MS45MzggMTI2LjM4NCAxNDIuMTYgMTI1LjkxOCAxNDIuMjk1IDEyNS40MjRDMTQyLjQ0NyAxMjQuODY3IDE0Mi40NDcgMTI0LjI2NCAxNDIuNDQ3IDEyMy4wNThWNjMuMTEyNkMxNDIuNDQ3IDU4LjI4NTEgMTQyLjQ0NyA1NS44NzE0IDE0MS41MDggNTQuMDI3NkMxNDAuNjgxIDUyLjQwNTcgMTM5LjM2MyA1MS4wODcgMTM3Ljc0MSA1MC4yNjA2QzEzNS44OTcgNDkuMzIxMSAxMzMuNDgzIDQ5LjMyMTEgMTI4LjY1NiA0OS4zMjExSDExMC44NTlDMTA2LjAzMSA0OS4zMjExIDEwMy42MTggNDkuMzIxMSAxMDEuNzc0IDUwLjI2MDZDMTAwLjE1MiA1MS4wODcgOTguODMzMyA1Mi40MDU3IDk4LjAwNjkgNTQuMDI3NloiIGZpbGw9IiNGRkZFRkIiLz4KPC9zdmc+Cg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQwIiBoZWlnaHQ9IjI0MCIgdmlld0JveD0iMCAwIDI0MCAyNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMjM5LjExNiAxMjAuNDE3QzIzOS4xMTYgNTQuNDE5MyAxODUuNjEzIDAuOTE2NTA0IDExOS42MTYgMC45MTY1MDRDNTMuNjE5IDAuOTE2NTA0IDAuMTE2MjExIDU0LjQxOTMgMC4xMTYyMTEgMTIwLjQxN0MwLjExNjIxMSAxODYuNDE3IDUzLjYxOSAyMzkuOTE3IDExOS42MTYgMjM5LjkxN0MxODUuNjEzIDIzOS45MTcgMjM5LjExNiAxODYuNDE3IDIzOS4xMTYgMTIwLjQxN1pNOTcuODY1OCA1NC4wMjc2Qzk2LjkyNjMgNTUuODcxNCA5Ni45MjYzIDU4LjI4NTEgOTYuOTI2MyA2My4xMTI2VjkwLjQ3MjlDOTYuOTI2MyA5MS42Nzg4IDk2LjkyNjMgOTIuMjgxNyA5Ny4wNzg1IDkyLjgzOTJDOTcuMjEzNCA5My4zMzMxIDk3LjQzNTIgOTMuNzk5IDk3LjczMzUgOTQuMjE1Qzk4LjA3MDIgOTQuNjg0NyA5OC41MzgxIDk1LjA2NDggOTkuNDc0MSA5NS44MjUxTDEwNi4zOTUgMTAxLjQ0N0MxMDcuNTIzIDEwMi4zNjQgMTA4LjA4NyAxMDIuODIyIDEwOC4yOTIgMTAzLjM3NEMxMDguNDcxIDEwMy44NTcgMTA4LjQ3MSAxMDQuMzkgMTA4LjI5MiAxMDQuODczQzEwOC4wODcgMTA1LjQyNSAxMDcuNTIzIDEwNS44ODMgMTA2LjM5NSAxMDYuOEw5OS40NzQxIDExMi40MjJDOTguNTM4MiAxMTMuMTgyIDk4LjA3MDIgMTEzLjU2MiA5Ny43MzM1IDExNC4wMzJDOTcuNDM1MiAxMTQuNDQ4IDk3LjIxMzQgMTE0LjkxNCA5Ny4wNzg1IDExNS40MDhDOTYuOTI2MyAxMTUuOTY1IDk2LjkyNjMgMTE2LjU2OCA5Ni45MjYzIDExNy43NzRWMTc3LjcxOUM5Ni45MjYzIDE4Mi41NDcgOTYuOTI2MyAxODQuOTYxIDk3Ljg2NTggMTg2LjgwNUM5OC42OTIyIDE4OC40MjYgMTAwLjAxMSAxODkuNzQ1IDEwMS42MzMgMTkwLjU3MUMxMDMuNDc3IDE5MS41MTEgMTA1Ljg5IDE5MS41MTEgMTEwLjcxOCAxOTEuNTExSDEyOC41MTVDMTMzLjM0MiAxOTEuNTExIDEzNS43NTYgMTkxLjUxMSAxMzcuNiAxOTAuNTcxQzEzOS4yMjEgMTg5Ljc0NSAxNDAuNTQgMTg4LjQyNiAxNDEuMzY3IDE4Ni44MDVDMTQyLjMwNiAxODQuOTYxIDE0Mi4zMDYgMTgyLjU0NyAxNDIuMzA2IDE3Ny43MTlWMTUwLjM1OUMxNDIuMzA2IDE0OS4xNTMgMTQyLjMwNiAxNDguNTUgMTQyLjE1NCAxNDcuOTkzQzE0Mi4wMTkgMTQ3LjQ5OSAxNDEuNzk3IDE0Ny4wMzMgMTQxLjQ5OSAxNDYuNjE3QzE0MS4xNjIgMTQ2LjE0NyAxNDAuNjk0IDE0NS43NjcgMTM5Ljc1OCAxNDUuMDA3TDEzMi44MzcgMTM5LjM4NUMxMzEuNzA5IDEzOC40NjggMTMxLjE0NSAxMzguMDEgMTMwLjk0IDEzNy40NTlDMTMwLjc2MSAxMzYuOTc1IDEzMC43NjEgMTM2LjQ0MyAxMzAuOTQgMTM1Ljk1OUMxMzEuMTQ1IDEzNS40MDcgMTMxLjcwOSAxMzQuOTQ5IDEzMi44MzcgMTM0LjAzM0wxMzkuNzU4IDEyOC40MUMxNDAuNjk0IDEyNy42NSAxNDEuMTYyIDEyNy4yNyAxNDEuNDk5IDEyNi44QzE0MS43OTcgMTI2LjM4NCAxNDIuMDE5IDEyNS45MTggMTQyLjE1NCAxMjUuNDI0QzE0Mi4zMDYgMTI0Ljg2NyAxNDIuMzA2IDEyNC4yNjQgMTQyLjMwNiAxMjMuMDU4VjYzLjExMjZDMTQyLjMwNiA1OC4yODUxIDE0Mi4zMDYgNTUuODcxNCAxNDEuMzY3IDU0LjAyNzZDMTQwLjU0IDUyLjQwNTcgMTM5LjIyMSA1MS4wODcgMTM3LjYgNTAuMjYwNkMxMzUuNzU2IDQ5LjMyMTEgMTMzLjM0MiA0OS4zMjExIDEyOC41MTUgNDkuMzIxMUgxMTAuNzE4QzEwNS44OSA0OS4zMjExIDEwMy40NzcgNDkuMzIxMSAxMDEuNjMzIDUwLjI2MDZDMTAwLjAxMSA1MS4wODcgOTguNjkyMiA1Mi40MDU3IDk3Ljg2NTggNTQuMDI3NloiIGZpbGw9IiMxQTI4NUYiLz4KPC9zdmc+Cg==" + }, + "b84e4048-15dc-4dd0-8640-f4f60813c8af": { + "name": "NordPass", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgODAgODAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik03LjYxMzQgNzBDMi44MjQzNSA2My4zNTIgMCA1NS4xNzIyIDAgNDYuMzI3M0MwIDI0LjA1NTIgMTcuOTA4NiA2IDQwIDZDNjIuMDkxNCA2IDgwIDI0LjA1NTIgODAgNDYuMzI3M0M4MCA1NS4xNzIxIDc3LjE3NTcgNjMuMzUxOCA3Mi4zODY3IDY5Ljk5OTlMNTMuMTc0NyAzOC41NDY2TDUxLjMxOTUgNDEuNzA0Nkw1My4yMDE4IDUwLjQ4NzdMNDAgMjcuNzE0N0wzMS44MzM0IDQxLjYxNjFMMzMuNzM0NiA1MC40ODc3TDI2LjgxNDcgMzguNTY0Nkw3LjYxMzQgNzBaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgODAgODAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik03LjYxMzQgNzBDMi44MjQzNSA2My4zNTIgMCA1NS4xNzIyIDAgNDYuMzI3M0MwIDI0LjA1NTIgMTcuOTA4NiA2IDQwIDZDNjIuMDkxNCA2IDgwIDI0LjA1NTIgODAgNDYuMzI3M0M4MCA1NS4xNzIxIDc3LjE3NTcgNjMuMzUxOCA3Mi4zODY3IDY5Ljk5OTlMNTMuMTc0NyAzOC41NDY2TDUxLjMxOTUgNDEuNzA0Nkw1My4yMDE4IDUwLjQ4NzdMNDAgMjcuNzE0N0wzMS44MzM0IDQxLjYxNjFMMzMuNzM0NiA1MC40ODc3TDI2LjgxNDcgMzguNTY0Nkw3LjYxMzQgNzBaIiBmaWxsPSIjMENBQUFCIi8+Cjwvc3ZnPgo=" + }, + "0ea242b4-43c4-4a1b-8b17-dd6d0b6baec6": { + "name": "Keeper", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzYwMzRfMzM2MjcpIj4KPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTIiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMiAxMkMyMiAxNy41MjI4IDE3LjUyMjggMjIgMTIgMjJDNi40NzcxNSAyMiAyIDE3LjUyMjggMiAxMkMyIDYuNDc3MTUgNi40NzcxNSAyIDEyIDJDMTcuNTIyOCAyIDIyIDYuNDc3MTUgMjIgMTJaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMTAuMTIxOCAzLjI3MzI1SDExLjY2NjZWOS41MTUyN0gxNC44NTc1TDE4LjY5NiA2LjQ2MzE3TDE5LjY2MDcgNy42NjgyMUwxNS4zOTg5IDExLjA1NjRIMTAuMTIxOFYzLjI3MzI1WiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNMTMuMTQzOCAzLjQ4MzY2TDE0LjY4ODcgMy44NzY5NFY2LjAzNDkyTDE2LjQxNzMgNC42MTgxMUwxNy43MDA4IDUuNTYwOTdMMTQuNDA3IDguMjYwMTNMMTMuMTQzOCA4LjI1MzQxVjMuNDgzNjZaIiBmaWxsPSIjRkZDNzAwIi8+CjxwYXRoIGQ9Ik00LjAzODcgMTUuMDg0OUw1LjU4MzU0IDE2LjM5NThWNy44MTQyN0w0LjAzODcgOS4yMjc3MlYxNS4wODQ5WiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNOC42MTI1NyAxOC4yNDExTDcuMDY2MDQgMTkuNTgwNlY0LjQ5NDg1TDguNjEyNTcgNS44MzQzNFYxOC4yNDExWiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNMTQuNjg4NyAxOC4xMTc0TDE2LjQxNzMgMTkuNTM0MkwxNy43MDA4IDE4LjU4OTdMMTQuNDA3IDE1Ljg5MjJMMTMuMTQzOCAxNS44OTg5VjIwLjY2ODdMMTQuNjg4NyAyMC4yNzU0VjE4LjExNzRaIiBmaWxsPSIjRkZDNzAwIi8+CjxwYXRoIGQ9Ik0xOC42OTYgMTcuNDc4NkwxNC44NTc1IDE0LjQyNDhIMTEuNjY2NlYyMC42NjY4SDEwLjEyMThWMTIuODg1M0gxNS4zOTg5TDE5LjY2MDcgMTYuMjczNUwxOC42OTYgMTcuNDc4NloiIGZpbGw9IiNGRkM3MDAiLz4KPHBhdGggZD0iTTE2LjczNzYgMTEuOTcwNkwxOS44OTgxIDE0LjU3MDZMMjAuODgzIDEzLjM4MjNMMTkuMTY2MSAxMS45NzA2TDIwLjg4MyAxMC41NTg4TDE5Ljg5ODEgOS4zNzA1NkwxNi43Mzc2IDExLjk3MDZaIiBmaWxsPSIjRkZDNzAwIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNjAzNF8zMzYyNyI+CjxyZWN0IHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzYwMzRfMzM2MjcpIj4KPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTIiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMiAxMkMyMiAxNy41MjI4IDE3LjUyMjggMjIgMTIgMjJDNi40NzcxNSAyMiAyIDE3LjUyMjggMiAxMkMyIDYuNDc3MTUgNi40NzcxNSAyIDEyIDJDMTcuNTIyOCAyIDIyIDYuNDc3MTUgMjIgMTJaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMTAuMTIxOCAzLjI3MzI1SDExLjY2NjZWOS41MTUyN0gxNC44NTc1TDE4LjY5NiA2LjQ2MzE3TDE5LjY2MDcgNy42NjgyMUwxNS4zOTg5IDExLjA1NjRIMTAuMTIxOFYzLjI3MzI1WiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNMTMuMTQzOCAzLjQ4MzY2TDE0LjY4ODcgMy44NzY5NFY2LjAzNDkyTDE2LjQxNzMgNC42MTgxMUwxNy43MDA4IDUuNTYwOTdMMTQuNDA3IDguMjYwMTNMMTMuMTQzOCA4LjI1MzQxVjMuNDgzNjZaIiBmaWxsPSIjRkZDNzAwIi8+CjxwYXRoIGQ9Ik00LjAzODcgMTUuMDg0OUw1LjU4MzU0IDE2LjM5NThWNy44MTQyN0w0LjAzODcgOS4yMjc3MlYxNS4wODQ5WiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNOC42MTI1NyAxOC4yNDExTDcuMDY2MDQgMTkuNTgwNlY0LjQ5NDg1TDguNjEyNTcgNS44MzQzNFYxOC4yNDExWiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNMTQuNjg4NyAxOC4xMTc0TDE2LjQxNzMgMTkuNTM0MkwxNy43MDA4IDE4LjU4OTdMMTQuNDA3IDE1Ljg5MjJMMTMuMTQzOCAxNS44OTg5VjIwLjY2ODdMMTQuNjg4NyAyMC4yNzU0VjE4LjExNzRaIiBmaWxsPSIjRkZDNzAwIi8+CjxwYXRoIGQ9Ik0xOC42OTYgMTcuNDc4NkwxNC44NTc1IDE0LjQyNDhIMTEuNjY2NlYyMC42NjY4SDEwLjEyMThWMTIuODg1M0gxNS4zOTg5TDE5LjY2MDcgMTYuMjczNUwxOC42OTYgMTcuNDc4NloiIGZpbGw9IiNGRkM3MDAiLz4KPHBhdGggZD0iTTE2LjczNzYgMTEuOTcwNkwxOS44OTgxIDE0LjU3MDZMMjAuODgzIDEzLjM4MjNMMTkuMTY2MSAxMS45NzA2TDIwLjg4MyAxMC41NTg4TDE5Ljg5ODEgOS4zNzA1NkwxNi43Mzc2IDExLjk3MDZaIiBmaWxsPSIjRkZDNzAwIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNjAzNF8zMzYyNyI+CjxyZWN0IHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K" + }, + "f3809540-7f14-49c1-a8b3-8f813b225541": { + "name": "Enpass", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yNTYuNDgzIDI4LjA1NTRDMzEzLjg5OSAyOC4wNTU0IDM3MS4zMTUgMjcuODg1NiA0MjguNzQ1IDI4LjE0MDVDNDQwLjY4IDI3LjkwNzYgNDUyLjUyMSAzMC4yODg5IDQ2My40NDEgMzUuMTE3OUM0NzQuMzYyIDM5Ljk0NjkgNDg0LjA5OSA0Ny4xMDczIDQ5MS45NzEgNTYuMDk4NEM1MDQuMDYyIDY5LjY5NjIgNTExLjEzMiA4Ny4wMzg3IDUxMiAxMDUuMjNDNTEyLjAyOCAxMjEuOTMzIDUxMC4wMzUgMTM4LjU3OCA1MDYuMDYzIDE1NC44MDFDNDk4LjQ0NCAxOTguNzA2IDQ5MC41MTUgMjQyLjUyNyA0ODIuNzI2IDI4Ni4zNzZDNDc2LjAxMiAzMjQuMTM0IDQ2OS41ODEgMzYxLjk1IDQ2Mi41MTMgMzk5LjY4QzQ1Ny42NzIgNDIwLjk2OSA0NDYuNTQ1IDQ0MC4zMDMgNDMwLjU4IDQ1NS4xNjVDNDE0LjYxNiA0NzAuMDI3IDM5NC41NTUgNDc5LjcyNyAzNzMuMDExIDQ4My4wMDJDMzY3Ljc1MiA0ODMuNjI5IDM2Mi40NjIgNDgzLjk0NiAzNTcuMTY2IDQ4My45NUMyOTAuMDUzIDQ4NC4wMTcgMjIyLjk0IDQ4NC4wMTcgMTU1LjgyOCA0ODMuOTVDMTMwLjQ2NiA0ODMuOSAxMDUuOTMgNDc0LjkxNSA4Ni41MTMyIDQ1OC41NjZDNjcuMDk2NSA0NDIuMjE4IDU0LjAzNjIgNDE5LjU0OCA0OS42MTggMzk0LjUyNUMzNi4xODA0IDMxOS4xNzcgMjIuNjI5NyAyNDMuODUzIDguOTY1OTcgMTY4LjU1M0M2LjI4MDM0IDE1My42MzkgMy4zMTIgMTM4LjgxMSAxLjIwNTkgMTIzLjc4NEMtMi40NjEwNSAxMDIuNzI5IDIuMzEwOTMgODEuMDc0NCAxNC40ODUyIDYzLjUyNDRDMjYuNjU5NiA0NS45NzQ1IDQ1LjI1MjkgMzMuOTQ2MiA2Ni4yMjY2IDMwLjA1MjVDNzMuMDU1NyAyOC43NDUxIDc5Ljk5NTkgMjguMTA5NCA4Ni45NDg0IDI4LjE1NDZDMTQzLjQ2IDI3Ljk5NDEgMTk5Ljk3MSAyNy45NjEgMjU2LjQ4MyAyOC4wNTU0Wk0yMTAuOTI2IDMzOS42NDNDMjEwLjkyNiAzNTQuNjcgMjEwLjkyNiAzNjkuNjk3IDIxMC45MjYgMzg0LjczOEMyMTAuNzczIDM4OC4yMDUgMjExLjM0MyAzOTEuNjY1IDIxMi41OTcgMzk0Ljg5OUMyMTMuODUyIDM5OC4xMzQgMjE1Ljc2NCA0MDEuMDcxIDIxOC4yMTMgNDAzLjUyNUMyMjAuNjYyIDQwNS45NzkgMjIzLjU5MyA0MDcuODk1IDIyNi44MjEgNDA5LjE1MkMyMzAuMDQ5IDQxMC40MDkgMjMzLjUwMyA0MTAuOTc5IDIzNi45NjIgNDEwLjgyNkMyNDkuMzg3IDQxMC44MjYgMjYxLjgxMiA0MTAuODI2IDI3NC4yMzYgNDEwLjgyNkMyNzcuOTIyIDQxMS4xODMgMjgxLjY0MiA0MTAuNzE3IDI4NS4xMjcgNDA5LjQ2MkMyODguNjEyIDQwOC4yMDggMjkxLjc3NyA0MDYuMTk2IDI5NC4zOTQgNDAzLjU3QzI5Ny4wMTIgNDAwLjk0NSAyOTkuMDE3IDM5Ny43NzIgMzAwLjI2NSAzOTQuMjc4QzMwMS41MTQgMzkwLjc4NSAzMDEuOTc1IDM4Ny4wNTggMzAxLjYxNSAzODMuMzY0QzMwMS42MTUgMzUzLjkxOSAzMDEuNjE2IDMyNC40NiAzMDEuNDc0IDI5NS4wMTVDMzAxLjMxMSAyOTMuMzMxIDMwMS42NyAyOTEuNjM3IDMwMi41MDIgMjkwLjE2NUMzMDMuMzM0IDI4OC42OTIgMzA0LjU5OSAyODcuNTEyIDMwNi4xMjUgMjg2Ljc4NkMzMjMuNzkgMjc2LjI5OCAzMzcuNTUxIDI2MC4zMTQgMzQ1LjMxMyAyNDEuMjY2QzM1My4wNzUgMjIyLjIxOSAzNTQuNDEzIDIwMS4xNTEgMzQ5LjEyMyAxODEuMjcyQzM0Mi4zNTYgMTU2Ljg1MyAzMjYuMjg2IDEzNi4wNzUgMzA0LjM3NiAxMjMuNDE0QzI4Mi40NjYgMTEwLjc1NCAyNTYuNDY5IDEwNy4yMjUgMjMxLjk4NyAxMTMuNTg2QzIxNy42NjkgMTE2LjU0NCAyMDQuMjg5IDEyMi45NTkgMTkzLjAwNyAxMzIuMjc0QzE4MS43MjYgMTQxLjU4OCAxNzIuODg0IDE1My41MjIgMTY3LjI0OSAxNjcuMDM4QzE1OS4wMjcgMTg4LjY4NiAxNTguNTQ4IDIxMi41MjEgMTY1Ljg5MyAyMzQuNDg0QzE3My4yMzggMjU2LjQ0NyAxODcuOTU0IDI3NS4xODEgMjA3LjUzMyAyODcuNDk1QzIwOC42NyAyODguMDM4IDIwOS42MTMgMjg4LjkxNyAyMTAuMjM3IDI5MC4wMTNDMjEwLjg2MSAyOTEuMTA5IDIxMS4xMzYgMjkyLjM3IDIxMS4wMjUgMjkzLjYyN0MyMTAuODQxIDMwOS4wMDggMjEwLjkyNiAzMjQuMzMzIDIxMC45MjYgMzM5LjY3MVYzMzkuNjQzWiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yNTYuNDgzIDI4LjA1NTRDMzEzLjg5OSAyOC4wNTU0IDM3MS4zMTUgMjcuODg1NiA0MjguNzQ1IDI4LjE0MDVDNDQwLjY4IDI3LjkwNzYgNDUyLjUyMSAzMC4yODg5IDQ2My40NDEgMzUuMTE3OUM0NzQuMzYyIDM5Ljk0NjkgNDg0LjA5OSA0Ny4xMDczIDQ5MS45NzEgNTYuMDk4NEM1MDQuMDYyIDY5LjY5NjIgNTExLjEzMiA4Ny4wMzg3IDUxMiAxMDUuMjNDNTEyLjAyOCAxMjEuOTMzIDUxMC4wMzUgMTM4LjU3OCA1MDYuMDYzIDE1NC44MDFDNDk4LjQ0NCAxOTguNzA2IDQ5MC41MTUgMjQyLjUyNyA0ODIuNzI2IDI4Ni4zNzZDNDc2LjAxMiAzMjQuMTM0IDQ2OS41ODEgMzYxLjk1IDQ2Mi41MTMgMzk5LjY4QzQ1Ny42NzIgNDIwLjk2OSA0NDYuNTQ1IDQ0MC4zMDMgNDMwLjU4IDQ1NS4xNjVDNDE0LjYxNiA0NzAuMDI3IDM5NC41NTUgNDc5LjcyNyAzNzMuMDExIDQ4My4wMDJDMzY3Ljc1MiA0ODMuNjI5IDM2Mi40NjIgNDgzLjk0NiAzNTcuMTY2IDQ4My45NUMyOTAuMDUzIDQ4NC4wMTcgMjIyLjk0IDQ4NC4wMTcgMTU1LjgyOCA0ODMuOTVDMTMwLjQ2NiA0ODMuOSAxMDUuOTMgNDc0LjkxNSA4Ni41MTMyIDQ1OC41NjZDNjcuMDk2NSA0NDIuMjE4IDU0LjAzNjIgNDE5LjU0OCA0OS42MTggMzk0LjUyNUMzNi4xODA0IDMxOS4xNzcgMjIuNjI5NyAyNDMuODUzIDguOTY1OTcgMTY4LjU1M0M2LjI4MDM0IDE1My42MzkgMy4zMTIgMTM4LjgxMSAxLjIwNTkgMTIzLjc4NEMtMi40NjEwNSAxMDIuNzI5IDIuMzEwOTMgODEuMDc0NCAxNC40ODUyIDYzLjUyNDRDMjYuNjU5NiA0NS45NzQ1IDQ1LjI1MjkgMzMuOTQ2MiA2Ni4yMjY2IDMwLjA1MjVDNzMuMDU1NyAyOC43NDUxIDc5Ljk5NTkgMjguMTA5NCA4Ni45NDg0IDI4LjE1NDZDMTQzLjQ2IDI3Ljk5NDEgMTk5Ljk3MSAyNy45NjEgMjU2LjQ4MyAyOC4wNTU0Wk0yMTAuOTI2IDMzOS42NDNDMjEwLjkyNiAzNTQuNjcgMjEwLjkyNiAzNjkuNjk3IDIxMC45MjYgMzg0LjczOEMyMTAuNzczIDM4OC4yMDUgMjExLjM0MyAzOTEuNjY1IDIxMi41OTcgMzk0Ljg5OUMyMTMuODUyIDM5OC4xMzQgMjE1Ljc2NCA0MDEuMDcxIDIxOC4yMTMgNDAzLjUyNUMyMjAuNjYyIDQwNS45NzkgMjIzLjU5MyA0MDcuODk1IDIyNi44MjEgNDA5LjE1MkMyMzAuMDQ5IDQxMC40MDkgMjMzLjUwMyA0MTAuOTc5IDIzNi45NjIgNDEwLjgyNkMyNDkuMzg3IDQxMC44MjYgMjYxLjgxMiA0MTAuODI2IDI3NC4yMzYgNDEwLjgyNkMyNzcuOTIyIDQxMS4xODMgMjgxLjY0MiA0MTAuNzE3IDI4NS4xMjcgNDA5LjQ2MkMyODguNjEyIDQwOC4yMDggMjkxLjc3NyA0MDYuMTk2IDI5NC4zOTQgNDAzLjU3QzI5Ny4wMTIgNDAwLjk0NSAyOTkuMDE3IDM5Ny43NzIgMzAwLjI2NSAzOTQuMjc4QzMwMS41MTQgMzkwLjc4NSAzMDEuOTc1IDM4Ny4wNTggMzAxLjYxNSAzODMuMzY0QzMwMS42MTUgMzUzLjkxOSAzMDEuNjE2IDMyNC40NiAzMDEuNDc0IDI5NS4wMTVDMzAxLjMxMSAyOTMuMzMxIDMwMS42NyAyOTEuNjM3IDMwMi41MDIgMjkwLjE2NUMzMDMuMzM0IDI4OC42OTIgMzA0LjU5OSAyODcuNTEyIDMwNi4xMjUgMjg2Ljc4NkMzMjMuNzkgMjc2LjI5OCAzMzcuNTUxIDI2MC4zMTQgMzQ1LjMxMyAyNDEuMjY2QzM1My4wNzUgMjIyLjIxOSAzNTQuNDEzIDIwMS4xNTEgMzQ5LjEyMyAxODEuMjcyQzM0Mi4zNTYgMTU2Ljg1MyAzMjYuMjg2IDEzNi4wNzUgMzA0LjM3NiAxMjMuNDE0QzI4Mi40NjYgMTEwLjc1NCAyNTYuNDY5IDEwNy4yMjUgMjMxLjk4NyAxMTMuNTg2QzIxNy42NjkgMTE2LjU0NCAyMDQuMjg5IDEyMi45NTkgMTkzLjAwNyAxMzIuMjc0QzE4MS43MjYgMTQxLjU4OCAxNzIuODg0IDE1My41MjIgMTY3LjI0OSAxNjcuMDM4QzE1OS4wMjcgMTg4LjY4NiAxNTguNTQ4IDIxMi41MjEgMTY1Ljg5MyAyMzQuNDg0QzE3My4yMzggMjU2LjQ0NyAxODcuOTU0IDI3NS4xODEgMjA3LjUzMyAyODcuNDk1QzIwOC42NyAyODguMDM4IDIwOS42MTMgMjg4LjkxNyAyMTAuMjM3IDI5MC4wMTNDMjEwLjg2MSAyOTEuMTA5IDIxMS4xMzYgMjkyLjM3IDIxMS4wMjUgMjkzLjYyN0MyMTAuODQxIDMwOS4wMDggMjEwLjkyNiAzMjQuMzMzIDIxMC45MjYgMzM5LjY3MVYzMzkuNjQzWiIgZmlsbD0iIzBEMzM4RiIvPgo8L3N2Zz4K" + }, + "b5397666-4885-aa6b-cebf-e52262a439a2": { + "name": "Chromium Browser" + }, + "771b48fd-d3d4-4f74-9232-fc157ab0507a": { + "name": "Edge on Mac", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOnVybCgjbGluZWFyLWdyYWRpZW50KTt9LmNscy0ye29wYWNpdHk6MC4zNTtmaWxsOnVybCgjcmFkaWFsLWdyYWRpZW50KTt9LmNscy0yLC5jbHMtNHtpc29sYXRpb246aXNvbGF0ZTt9LmNscy0ze2ZpbGw6dXJsKCNsaW5lYXItZ3JhZGllbnQtMik7fS5jbHMtNHtvcGFjaXR5OjAuNDE7ZmlsbDp1cmwoI3JhZGlhbC1ncmFkaWVudC0yKTt9LmNscy01e2ZpbGw6dXJsKCNyYWRpYWwtZ3JhZGllbnQtMyk7fS5jbHMtNntmaWxsOnVybCgjcmFkaWFsLWdyYWRpZW50LTQpO308L3N0eWxlPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50IiB4MT0iNjMuMzMiIHkxPSI4NC4wMyIgeDI9IjI0MS42NyIgeTI9Ijg0LjAzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIC0xLCAwLCAyNjYpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGM1OWE0Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTE0YThiIi8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudCIgY3g9IjE2MS44MyIgY3k9IjY4LjkxIiByPSI5NS4zOCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLCAwLCAwLCAtMC45NSwgMCwgMjQ4Ljg0KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMC43MiIgc3RvcC1vcGFjaXR5PSIwIi8+PHN0b3Agb2Zmc2V0PSIwLjk1IiBzdG9wLW9wYWNpdHk9IjAuNTMiLz48c3RvcCBvZmZzZXQ9IjEiLz48L3JhZGlhbEdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTIiIHgxPSIxNTcuMzUiIHkxPSIxNjEuMzkiIHgyPSI0NS45NiIgeTI9IjQwLjA2IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIC0xLCAwLCAyNjYpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMWI5ZGUyIi8+PHN0b3Agb2Zmc2V0PSIwLjE2IiBzdG9wLWNvbG9yPSIjMTU5NWRmIi8+PHN0b3Agb2Zmc2V0PSIwLjY3IiBzdG9wLWNvbG9yPSIjMDY4MGQ3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDA3OGQ0Ii8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudC0yIiBjeD0iLTM0MC4yOSIgY3k9IjYyLjk5IiByPSIxNDMuMjQiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMC4xNSwgLTAuOTksIC0wLjgsIC0wLjEyLCAxNzYuNjQsIC0xMjUuNCkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAuNzYiIHN0b3Atb3BhY2l0eT0iMCIvPjxzdG9wIG9mZnNldD0iMC45NSIgc3RvcC1vcGFjaXR5PSIwLjUiLz48c3RvcCBvZmZzZXQ9IjEiLz48L3JhZGlhbEdyYWRpZW50PjxyYWRpYWxHcmFkaWVudCBpZD0icmFkaWFsLWdyYWRpZW50LTMiIGN4PSIxMTMuMzciIGN5PSI1NzAuMjEiIHI9IjIwMi40MyIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgtMC4wNCwgMSwgMi4xMywgMC4wOCwgLTExNzkuNTQsIC0xMDYuNjkpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMzVjMWYxIi8+PHN0b3Agb2Zmc2V0PSIwLjExIiBzdG9wLWNvbG9yPSIjMzRjMWVkIi8+PHN0b3Agb2Zmc2V0PSIwLjIzIiBzdG9wLWNvbG9yPSIjMmZjMmRmIi8+PHN0b3Agb2Zmc2V0PSIwLjMxIiBzdG9wLWNvbG9yPSIjMmJjM2QyIi8+PHN0b3Agb2Zmc2V0PSIwLjY3IiBzdG9wLWNvbG9yPSIjMzZjNzUyIi8+PC9yYWRpYWxHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudC00IiBjeD0iMzc2LjUyIiBjeT0iNTY3Ljk3IiByPSI5Ny4zNCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgwLjI4LCAwLjk2LCAwLjc4LCAtMC4yMywgLTMwMy43NiwgLTE0OC41KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzY2ZWI2ZSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzY2ZWI2ZSIgc3RvcC1vcGFjaXR5PSIwIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHRpdGxlPkVkZ2VfTG9nb18yNjV4MjY1PC90aXRsZT48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0yMzUuNjgsMTk1LjQ2YTkzLjczLDkzLjczLDAsMCwxLTEwLjU0LDQuNzEsMTAxLjg3LDEwMS44NywwLDAsMS0zNS45LDYuNDZjLTQ3LjMyLDAtODguNTQtMzIuNTUtODguNTQtNzQuMzJBMzEuNDgsMzEuNDgsMCwwLDEsMTE3LjEzLDEwNWMtNDIuOCwxLjgtNTMuOCw0Ni40LTUzLjgsNzIuNTMsMCw3My44OCw2OC4wOSw4MS4zNyw4Mi43Niw4MS4zNyw3LjkxLDAsMTkuODQtMi4zLDI3LTQuNTZsMS4zMS0uNDRBMTI4LjM0LDEyOC4zNCwwLDAsMCwyNDEsMjAxLjEsNCw0LDAsMCwwLDIzNS42OCwxOTUuNDZaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMzUuNjgsMTk1LjQ2YTkzLjczLDkzLjczLDAsMCwxLTEwLjU0LDQuNzEsMTAxLjg3LDEwMS44NywwLDAsMS0zNS45LDYuNDZjLTQ3LjMyLDAtODguNTQtMzIuNTUtODguNTQtNzQuMzJBMzEuNDgsMzEuNDgsMCwwLDEsMTE3LjEzLDEwNWMtNDIuOCwxLjgtNTMuOCw0Ni40LTUzLjgsNzIuNTMsMCw3My44OCw2OC4wOSw4MS4zNyw4Mi43Niw4MS4zNyw3LjkxLDAsMTkuODQtMi4zLDI3LTQuNTZsMS4zMS0uNDRBMTI4LjM0LDEyOC4zNCwwLDAsMCwyNDEsMjAxLjEsNCw0LDAsMCwwLDIzNS42OCwxOTUuNDZaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik0xMTAuMzQsMjQ2LjM0QTc5LjIsNzkuMiwwLDAsMSw4Ny42LDIyNSw4MC43Miw4MC43MiwwLDAsMSwxMTcuMTMsMTA1YzMuMTItMS40Nyw4LjQ1LTQuMTMsMTUuNTQtNGEzMi4zNSwzMi4zNSwwLDAsMSwyNS42OSwxMywzMS44OCwzMS44OCwwLDAsMSw2LjM2LDE4LjY2YzAtLjIxLDI0LjQ2LTc5LjYtODAtNzkuNi00My45LDAtODAsNDEuNjYtODAsNzguMjFhMTMwLjE1LDEzMC4xNSwwLDAsMCwxMi4xMSw1NiwxMjgsMTI4LDAsMCwwLDE1Ni4zOCw2Ny4xMSw3NS41NSw3NS41NSwwLDAsMS02Mi43OC04WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQuNjMgLTQuOTIpIi8+PHBhdGggY2xhc3M9ImNscy00IiBkPSJNMTEwLjM0LDI0Ni4zNEE3OS4yLDc5LjIsMCwwLDEsODcuNiwyMjUsODAuNzIsODAuNzIsMCwwLDEsMTE3LjEzLDEwNWMzLjEyLTEuNDcsOC40NS00LjEzLDE1LjU0LTRhMzIuMzUsMzIuMzUsMCwwLDEsMjUuNjksMTMsMzEuODgsMzEuODgsMCwwLDEsNi4zNiwxOC42NmMwLS4yMSwyNC40Ni03OS42LTgwLTc5LjYtNDMuOSwwLTgwLDQxLjY2LTgwLDc4LjIxYTEzMC4xNSwxMzAuMTUsMCwwLDAsMTIuMTEsNTYsMTI4LDEyOCwwLDAsMCwxNTYuMzgsNjcuMTEsNzUuNTUsNzUuNTUsMCwwLDEtNjIuNzgtOFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC00LjYzIC00LjkyKSIvPjxwYXRoIGNsYXNzPSJjbHMtNSIgZD0iTTE1Ni45NCwxNTMuNzhjLS44MSwxLjA1LTMuMywyLjUtMy4zLDUuNjYsMCwyLjYxLDEuNyw1LjEyLDQuNzIsNy4yMywxNC4zOCwxMCw0MS40OSw4LjY4LDQxLjU2LDguNjhBNTkuNTYsNTkuNTYsMCwwLDAsMjMwLjE5LDE2N2E2MS4zOCw2MS4zOCwwLDAsMCwzMC40My01Mi44OGMuMjYtMjIuNDEtOC0zNy4zMS0xMS4zNC00My45MUMyMjguMDksMjguNzYsMTgyLjM1LDQuOTIsMTMyLjYxLDQuOTJhMTI4LDEyOCwwLDAsMC0xMjgsMTI2LjJjLjQ4LTM2LjU0LDM2LjgtNjYuMDUsODAtNjYuMDUsMy41LDAsMjMuNDYuMzQsNDIsMTAuMDcsMTYuMzQsOC41OCwyNC45LDE4Ljk0LDMwLjg1LDI5LjIxLDYuMTgsMTAuNjcsNy4yOCwyNC4xNSw3LjI4LDI5LjUyUzE2MiwxNDcuMiwxNTYuOTQsMTUzLjc4WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQuNjMgLTQuOTIpIi8+PHBhdGggY2xhc3M9ImNscy02IiBkPSJNMTU2Ljk0LDE1My43OGMtLjgxLDEuMDUtMy4zLDIuNS0zLjMsNS42NiwwLDIuNjEsMS43LDUuMTIsNC43Miw3LjIzLDE0LjM4LDEwLDQxLjQ5LDguNjgsNDEuNTYsOC42OEE1OS41Niw1OS41NiwwLDAsMCwyMzAuMTksMTY3YTYxLjM4LDYxLjM4LDAsMCwwLDMwLjQzLTUyLjg4Yy4yNi0yMi40MS04LTM3LjMxLTExLjM0LTQzLjkxQzIyOC4wOSwyOC43NiwxODIuMzUsNC45MiwxMzIuNjEsNC45MmExMjgsMTI4LDAsMCwwLTEyOCwxMjYuMmMuNDgtMzYuNTQsMzYuOC02Ni4wNSw4MC02Ni4wNSwzLjUsMCwyMy40Ni4zNCw0MiwxMC4wNywxNi4zNCw4LjU4LDI0LjksMTguOTQsMzAuODUsMjkuMjEsNi4xOCwxMC42Nyw3LjI4LDI0LjE1LDcuMjgsMjkuNTJTMTYyLDE0Ny4yLDE1Ni45NCwxNTMuNzhaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48L3N2Zz4=", + "icon_light": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOnVybCgjbGluZWFyLWdyYWRpZW50KTt9LmNscy0ye29wYWNpdHk6MC4zNTtmaWxsOnVybCgjcmFkaWFsLWdyYWRpZW50KTt9LmNscy0yLC5jbHMtNHtpc29sYXRpb246aXNvbGF0ZTt9LmNscy0ze2ZpbGw6dXJsKCNsaW5lYXItZ3JhZGllbnQtMik7fS5jbHMtNHtvcGFjaXR5OjAuNDE7ZmlsbDp1cmwoI3JhZGlhbC1ncmFkaWVudC0yKTt9LmNscy01e2ZpbGw6dXJsKCNyYWRpYWwtZ3JhZGllbnQtMyk7fS5jbHMtNntmaWxsOnVybCgjcmFkaWFsLWdyYWRpZW50LTQpO308L3N0eWxlPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50IiB4MT0iNjMuMzMiIHkxPSI4NC4wMyIgeDI9IjI0MS42NyIgeTI9Ijg0LjAzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIC0xLCAwLCAyNjYpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGM1OWE0Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTE0YThiIi8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudCIgY3g9IjE2MS44MyIgY3k9IjY4LjkxIiByPSI5NS4zOCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLCAwLCAwLCAtMC45NSwgMCwgMjQ4Ljg0KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMC43MiIgc3RvcC1vcGFjaXR5PSIwIi8+PHN0b3Agb2Zmc2V0PSIwLjk1IiBzdG9wLW9wYWNpdHk9IjAuNTMiLz48c3RvcCBvZmZzZXQ9IjEiLz48L3JhZGlhbEdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTIiIHgxPSIxNTcuMzUiIHkxPSIxNjEuMzkiIHgyPSI0NS45NiIgeTI9IjQwLjA2IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIC0xLCAwLCAyNjYpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMWI5ZGUyIi8+PHN0b3Agb2Zmc2V0PSIwLjE2IiBzdG9wLWNvbG9yPSIjMTU5NWRmIi8+PHN0b3Agb2Zmc2V0PSIwLjY3IiBzdG9wLWNvbG9yPSIjMDY4MGQ3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDA3OGQ0Ii8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudC0yIiBjeD0iLTM0MC4yOSIgY3k9IjYyLjk5IiByPSIxNDMuMjQiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMC4xNSwgLTAuOTksIC0wLjgsIC0wLjEyLCAxNzYuNjQsIC0xMjUuNCkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAuNzYiIHN0b3Atb3BhY2l0eT0iMCIvPjxzdG9wIG9mZnNldD0iMC45NSIgc3RvcC1vcGFjaXR5PSIwLjUiLz48c3RvcCBvZmZzZXQ9IjEiLz48L3JhZGlhbEdyYWRpZW50PjxyYWRpYWxHcmFkaWVudCBpZD0icmFkaWFsLWdyYWRpZW50LTMiIGN4PSIxMTMuMzciIGN5PSI1NzAuMjEiIHI9IjIwMi40MyIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgtMC4wNCwgMSwgMi4xMywgMC4wOCwgLTExNzkuNTQsIC0xMDYuNjkpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMzVjMWYxIi8+PHN0b3Agb2Zmc2V0PSIwLjExIiBzdG9wLWNvbG9yPSIjMzRjMWVkIi8+PHN0b3Agb2Zmc2V0PSIwLjIzIiBzdG9wLWNvbG9yPSIjMmZjMmRmIi8+PHN0b3Agb2Zmc2V0PSIwLjMxIiBzdG9wLWNvbG9yPSIjMmJjM2QyIi8+PHN0b3Agb2Zmc2V0PSIwLjY3IiBzdG9wLWNvbG9yPSIjMzZjNzUyIi8+PC9yYWRpYWxHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudC00IiBjeD0iMzc2LjUyIiBjeT0iNTY3Ljk3IiByPSI5Ny4zNCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgwLjI4LCAwLjk2LCAwLjc4LCAtMC4yMywgLTMwMy43NiwgLTE0OC41KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzY2ZWI2ZSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzY2ZWI2ZSIgc3RvcC1vcGFjaXR5PSIwIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHRpdGxlPkVkZ2VfTG9nb18yNjV4MjY1PC90aXRsZT48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0yMzUuNjgsMTk1LjQ2YTkzLjczLDkzLjczLDAsMCwxLTEwLjU0LDQuNzEsMTAxLjg3LDEwMS44NywwLDAsMS0zNS45LDYuNDZjLTQ3LjMyLDAtODguNTQtMzIuNTUtODguNTQtNzQuMzJBMzEuNDgsMzEuNDgsMCwwLDEsMTE3LjEzLDEwNWMtNDIuOCwxLjgtNTMuOCw0Ni40LTUzLjgsNzIuNTMsMCw3My44OCw2OC4wOSw4MS4zNyw4Mi43Niw4MS4zNyw3LjkxLDAsMTkuODQtMi4zLDI3LTQuNTZsMS4zMS0uNDRBMTI4LjM0LDEyOC4zNCwwLDAsMCwyNDEsMjAxLjEsNCw0LDAsMCwwLDIzNS42OCwxOTUuNDZaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMzUuNjgsMTk1LjQ2YTkzLjczLDkzLjczLDAsMCwxLTEwLjU0LDQuNzEsMTAxLjg3LDEwMS44NywwLDAsMS0zNS45LDYuNDZjLTQ3LjMyLDAtODguNTQtMzIuNTUtODguNTQtNzQuMzJBMzEuNDgsMzEuNDgsMCwwLDEsMTE3LjEzLDEwNWMtNDIuOCwxLjgtNTMuOCw0Ni40LTUzLjgsNzIuNTMsMCw3My44OCw2OC4wOSw4MS4zNyw4Mi43Niw4MS4zNyw3LjkxLDAsMTkuODQtMi4zLDI3LTQuNTZsMS4zMS0uNDRBMTI4LjM0LDEyOC4zNCwwLDAsMCwyNDEsMjAxLjEsNCw0LDAsMCwwLDIzNS42OCwxOTUuNDZaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik0xMTAuMzQsMjQ2LjM0QTc5LjIsNzkuMiwwLDAsMSw4Ny42LDIyNSw4MC43Miw4MC43MiwwLDAsMSwxMTcuMTMsMTA1YzMuMTItMS40Nyw4LjQ1LTQuMTMsMTUuNTQtNGEzMi4zNSwzMi4zNSwwLDAsMSwyNS42OSwxMywzMS44OCwzMS44OCwwLDAsMSw2LjM2LDE4LjY2YzAtLjIxLDI0LjQ2LTc5LjYtODAtNzkuNi00My45LDAtODAsNDEuNjYtODAsNzguMjFhMTMwLjE1LDEzMC4xNSwwLDAsMCwxMi4xMSw1NiwxMjgsMTI4LDAsMCwwLDE1Ni4zOCw2Ny4xMSw3NS41NSw3NS41NSwwLDAsMS02Mi43OC04WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQuNjMgLTQuOTIpIi8+PHBhdGggY2xhc3M9ImNscy00IiBkPSJNMTEwLjM0LDI0Ni4zNEE3OS4yLDc5LjIsMCwwLDEsODcuNiwyMjUsODAuNzIsODAuNzIsMCwwLDEsMTE3LjEzLDEwNWMzLjEyLTEuNDcsOC40NS00LjEzLDE1LjU0LTRhMzIuMzUsMzIuMzUsMCwwLDEsMjUuNjksMTMsMzEuODgsMzEuODgsMCwwLDEsNi4zNiwxOC42NmMwLS4yMSwyNC40Ni03OS42LTgwLTc5LjYtNDMuOSwwLTgwLDQxLjY2LTgwLDc4LjIxYTEzMC4xNSwxMzAuMTUsMCwwLDAsMTIuMTEsNTYsMTI4LDEyOCwwLDAsMCwxNTYuMzgsNjcuMTEsNzUuNTUsNzUuNTUsMCwwLDEtNjIuNzgtOFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC00LjYzIC00LjkyKSIvPjxwYXRoIGNsYXNzPSJjbHMtNSIgZD0iTTE1Ni45NCwxNTMuNzhjLS44MSwxLjA1LTMuMywyLjUtMy4zLDUuNjYsMCwyLjYxLDEuNyw1LjEyLDQuNzIsNy4yMywxNC4zOCwxMCw0MS40OSw4LjY4LDQxLjU2LDguNjhBNTkuNTYsNTkuNTYsMCwwLDAsMjMwLjE5LDE2N2E2MS4zOCw2MS4zOCwwLDAsMCwzMC40My01Mi44OGMuMjYtMjIuNDEtOC0zNy4zMS0xMS4zNC00My45MUMyMjguMDksMjguNzYsMTgyLjM1LDQuOTIsMTMyLjYxLDQuOTJhMTI4LDEyOCwwLDAsMC0xMjgsMTI2LjJjLjQ4LTM2LjU0LDM2LjgtNjYuMDUsODAtNjYuMDUsMy41LDAsMjMuNDYuMzQsNDIsMTAuMDcsMTYuMzQsOC41OCwyNC45LDE4Ljk0LDMwLjg1LDI5LjIxLDYuMTgsMTAuNjcsNy4yOCwyNC4xNSw3LjI4LDI5LjUyUzE2MiwxNDcuMiwxNTYuOTQsMTUzLjc4WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQuNjMgLTQuOTIpIi8+PHBhdGggY2xhc3M9ImNscy02IiBkPSJNMTU2Ljk0LDE1My43OGMtLjgxLDEuMDUtMy4zLDIuNS0zLjMsNS42NiwwLDIuNjEsMS43LDUuMTIsNC43Miw3LjIzLDE0LjM4LDEwLDQxLjQ5LDguNjgsNDEuNTYsOC42OEE1OS41Niw1OS41NiwwLDAsMCwyMzAuMTksMTY3YTYxLjM4LDYxLjM4LDAsMCwwLDMwLjQzLTUyLjg4Yy4yNi0yMi40MS04LTM3LjMxLTExLjM0LTQzLjkxQzIyOC4wOSwyOC43NiwxODIuMzUsNC45MiwxMzIuNjEsNC45MmExMjgsMTI4LDAsMCwwLTEyOCwxMjYuMmMuNDgtMzYuNTQsMzYuOC02Ni4wNSw4MC02Ni4wNSwzLjUsMCwyMy40Ni4zNCw0MiwxMC4wNywxNi4zNCw4LjU4LDI0LjksMTguOTQsMzAuODUsMjkuMjEsNi4xOCwxMC42Nyw3LjI4LDI0LjE1LDcuMjgsMjkuNTJTMTYyLDE0Ny4yLDE1Ni45NCwxNTMuNzhaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48L3N2Zz4=" + }, + "39a5647e-1853-446c-a1f6-a79bae9f5bc7": { + "name": "IDmelon Android Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAM1BMVEUtmc3y+fyWzOZis9rK5fI6n9B8v+Cw2ezl8vlHptNVrNbX7Paj0ulvud293++JxuP///89HRvpAAAAEXRSTlP/////////////////////ACWtmWIAAABsSURBVHgBxdPBCoAwDIPh/yDise//tIIQCZo6RNGdtuWDstFSg/UOgMiADQBJ6J4iCwS4BgzBuEQHCoFa+mdM+qijsDMVhBfdoRFaAL4nAe6AeghODYPnsaNyLuAqg5AHwO9AYu5BmqEPhncFmecvM5KKQHMAAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAM1BMVEUtmc3y+fyWzOZis9rK5fI6n9B8v+Cw2ezl8vlHptNVrNbX7Paj0ulvud293++JxuP///89HRvpAAAAEXRSTlP/////////////////////ACWtmWIAAABsSURBVHgBxdPBCoAwDIPh/yDise//tIIQCZo6RNGdtuWDstFSg/UOgMiADQBJ6J4iCwS4BgzBuEQHCoFa+mdM+qijsDMVhBfdoRFaAL4nAe6AeghODYPnsaNyLuAqg5AHwO9AYu5BmqEPhncFmecvM5KKQHMAAAAASUVORK5CYII=" + }, + "6e8248d5-b479-40db-a3d8-11116f7e8349": { + "name": "Bitwarden", + "icon_dark": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjAuMywgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9Ikljb24iIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDEwMjQgMTAyNDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiMxNzVEREM7fQoJLnN0MXtmaWxsOiNGRkZGRkY7fQo8L3N0eWxlPgo8cmVjdCBpZD0iQmFja2dyb3VuZCIgY2xhc3M9InN0MCIgd2lkdGg9IjEwMjQiIGhlaWdodD0iMTAyNCIvPgo8cGF0aCBpZD0iSWRlbnRpdHkiIGNsYXNzPSJzdDEiIGQ9Ik04MjkuOCwxMjguNmMtNi41LTYuNS0xNC4yLTkuNy0yMy05LjdIMjE3LjJjLTguOSwwLTE2LjUsMy4yLTIzLDkuN3MtOS43LDE0LjItOS43LDIzdjM5My4xCgljMCwyOS4zLDUuNyw1OC40LDE3LjEsODcuM2MxMS40LDI4LjgsMjUuNiw1NC40LDQyLjUsNzYuOGMxNi45LDIyLjMsMzcsNDQuMSw2MC40LDY1LjNzNDUsMzguNyw2NC43LDUyLjcKCWMxOS44LDE0LDQwLjQsMjcuMiw2MS45LDM5LjdzMzYuOCwyMC45LDQ1LjgsMjUuM2M5LDQuNCwxNi4zLDcuOSwyMS43LDEwLjJjNC4xLDIsOC41LDMuMSwxMy4zLDMuMWM0LjgsMCw5LjItMSwxMy4zLTMuMQoJYzUuNS0yLjQsMTIuNy01LjgsMjEuOC0xMC4yYzktNC40LDI0LjMtMTIuOSw0NS44LTI1LjNjMjEuNS0xMi41LDQyLjEtMjUuNyw2MS45LTM5LjdjMTkuOC0xNCw0MS40LTMxLjYsNjQuOC01Mi43CgljMjMuNC0yMS4yLDQzLjUtNDIuOSw2MC40LTY1LjNjMTYuOS0yMi40LDMxLTQ3LjksNDIuNS03Ni44YzExLjQtMjguOCwxNy4xLTU3LjksMTcuMS04Ny4zdi0zOTMKCUM4MzkuNiwxNDIuOCw4MzYuMywxMzUuMSw4MjkuOCwxMjguNnogTTc1My44LDU0OC40YzAsMTQyLjMtMjQxLjgsMjY0LjktMjQxLjgsMjY0LjlWMjAzaDI0MS44Qzc1My44LDIwMyw3NTMuOCw0MDYuMSw3NTMuOCw1NDguNHoKCSIvPgo8L3N2Zz4K", + "icon_light": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjAuMywgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9Ikljb24iIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDEwMjQgMTAyNDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiMxNzVEREM7fQoJLnN0MXtmaWxsOiNGRkZGRkY7fQo8L3N0eWxlPgo8cmVjdCBpZD0iQmFja2dyb3VuZCIgY2xhc3M9InN0MCIgd2lkdGg9IjEwMjQiIGhlaWdodD0iMTAyNCIvPgo8cGF0aCBpZD0iSWRlbnRpdHkiIGNsYXNzPSJzdDEiIGQ9Ik04MjkuOCwxMjguNmMtNi41LTYuNS0xNC4yLTkuNy0yMy05LjdIMjE3LjJjLTguOSwwLTE2LjUsMy4yLTIzLDkuN3MtOS43LDE0LjItOS43LDIzdjM5My4xCgljMCwyOS4zLDUuNyw1OC40LDE3LjEsODcuM2MxMS40LDI4LjgsMjUuNiw1NC40LDQyLjUsNzYuOGMxNi45LDIyLjMsMzcsNDQuMSw2MC40LDY1LjNzNDUsMzguNyw2NC43LDUyLjcKCWMxOS44LDE0LDQwLjQsMjcuMiw2MS45LDM5LjdzMzYuOCwyMC45LDQ1LjgsMjUuM2M5LDQuNCwxNi4zLDcuOSwyMS43LDEwLjJjNC4xLDIsOC41LDMuMSwxMy4zLDMuMWM0LjgsMCw5LjItMSwxMy4zLTMuMQoJYzUuNS0yLjQsMTIuNy01LjgsMjEuOC0xMC4yYzktNC40LDI0LjMtMTIuOSw0NS44LTI1LjNjMjEuNS0xMi41LDQyLjEtMjUuNyw2MS45LTM5LjdjMTkuOC0xNCw0MS40LTMxLjYsNjQuOC01Mi43CgljMjMuNC0yMS4yLDQzLjUtNDIuOSw2MC40LTY1LjNjMTYuOS0yMi40LDMxLTQ3LjksNDIuNS03Ni44YzExLjQtMjguOCwxNy4xLTU3LjksMTcuMS04Ny4zdi0zOTMKCUM4MzkuNiwxNDIuOCw4MzYuMywxMzUuMSw4MjkuOCwxMjguNnogTTc1My44LDU0OC40YzAsMTQyLjMtMjQxLjgsMjY0LjktMjQxLjgsMjY0LjlWMjAzaDI0MS44Qzc1My44LDIwMyw3NTMuOCw0MDYuMSw3NTMuOCw1NDguNHoKCSIvPgo8L3N2Zz4K" + }, + "fcb1bcb4-f370-078c-6993-bc24d0ae3fbe": { + "name": "Ledger Nano X FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "9c835346-796b-4c27-8898-d6032f515cc5": { + "name": "Cryptnox FIDO2", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "c5ef55ff-ad9a-4b9f-b580-adebafe026d0": { + "name": "YubiKey 5 Series with Lightning", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "3789da91-f943-46bc-95c3-50ea2012f03a": { + "name": "NEOWAVE Winkeo FIDO2", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAACqUlEQVRIx2P8//8/Ay0BEwONwagFpFlw8cKFirIyR3t7S1Oz0KDgBfPm//z5k3izvn39lp+Ta2tltWTRIoTofxhYtXKllpq6srwCAikoRIVHvH379j9x4NSpU0AtQI1W5hZwQagPzp87V11ZiXAvIxj9Zzh54kRNZRWRPvj96xcDOM0zMTKiB9G8uXP//fsHNFRASLC+sXHm7Nlubu4Qm3bt3Llu7VpiLGCEmcuIacGZU6fB4cWQX1AQGx/n7OIyaeoUbV0diIvamluePXtGUST/+g32HSODhoYGRISFhaWppYWVlRUo+OHjh6b6BoosgHvqz58/cDl9ff3M7CwIe8+e3atXrqQgmeIokDKzs/X19EGy/xk6OzofP3pEWUbDsAYYRC3tbRwcHED2h/fv62pqCReOjCTmZE0trZy8XAj78KFDy5YuJd50VAsYcepKTU83NjWBqOnu7Hxw/wE+O/7jsgC315mZmRubm9nZ2YFqvnz+0lBfhzOg/qO7lQm/B+EAmHwLioogCo4cOrxk0WIiPUEgkpFBUnKymZk5hN3T1XX3zh1iYoKJcDTBA4qFubmtlYubC8j++vVrTVU1qHQhzQeMBHyhrKxcWFwMUXn61Kn5c+dSv8JJSEy0trGGsCf099+6dQsuxcLCCrH7P5IrSYgDeKFS39TEx8sHZH//9r2uGhFQN65fh2VPNoqqTCUlpeKyUmgxfPpMSWERMAMuX7asv7cXIqilrYXwFrxeg/qOuGZSdEzM3t17Dh06CPT0pk0bN23cCI9FYKZJz8hE98Hff38hDDY2diL90dHdpaurixawrCysre3tunq6iLTX0NAAToIsTx4/tndwiIyOAtYExFjAzc3t4+sLJL99/QosE0VFRe3s7RtbmoGVFUqcjTYdh78FAIhBLlNd7ju1AAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAACqUlEQVRIx2P8//8/Ay0BEwONwagFpFlw8cKFirIyR3t7S1Oz0KDgBfPm//z5k3izvn39lp+Ta2tltWTRIoTofxhYtXKllpq6srwCAikoRIVHvH379j9x4NSpU0AtQI1W5hZwQagPzp87V11ZiXAvIxj9Zzh54kRNZRWRPvj96xcDOM0zMTKiB9G8uXP//fsHNFRASLC+sXHm7Nlubu4Qm3bt3Llu7VpiLGCEmcuIacGZU6fB4cWQX1AQGx/n7OIyaeoUbV0diIvamluePXtGUST/+g32HSODhoYGRISFhaWppYWVlRUo+OHjh6b6BoosgHvqz58/cDl9ff3M7CwIe8+e3atXrqQgmeIokDKzs/X19EGy/xk6OzofP3pEWUbDsAYYRC3tbRwcHED2h/fv62pqCReOjCTmZE0trZy8XAj78KFDy5YuJd50VAsYcepKTU83NjWBqOnu7Hxw/wE+O/7jsgC315mZmRubm9nZ2YFqvnz+0lBfhzOg/qO7lQm/B+EAmHwLioogCo4cOrxk0WIiPUEgkpFBUnKymZk5hN3T1XX3zh1iYoKJcDTBA4qFubmtlYubC8j++vVrTVU1qHQhzQeMBHyhrKxcWFwMUXn61Kn5c+dSv8JJSEy0trGGsCf099+6dQsuxcLCCrH7P5IrSYgDeKFS39TEx8sHZH//9r2uGhFQN65fh2VPNoqqTCUlpeKyUmgxfPpMSWERMAMuX7asv7cXIqilrYXwFrxeg/qOuGZSdEzM3t17Dh06CPT0pk0bN23cCI9FYKZJz8hE98Hff38hDDY2diL90dHdpaurixawrCysre3tunq6iLTX0NAAToIsTx4/tndwiIyOAtYExFjAzc3t4+sLJL99/QosE0VFRe3s7RtbmoGVFUqcjTYdh78FAIhBLlNd7ju1AAAAAElFTkSuQmCC" + }, + "fa2b99dc-9e39-4257-8f92-4a30d23c4118": { + "name": "YubiKey 5 Series with NFC", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "69700f79-d1fb-472e-bd9b-a3a3b9a9eda0": { + "name": "Pone Biometrics OFFPAD Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAaMAAAGjCAYAAACBlXr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAHTmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4wLWMwMDAgNzkuMTcxYzI3ZmFiLCAyMDIyLzA4LzE2LTIyOjM1OjQxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo3YWY3MjAyNS0yZDJhLTZjNGEtOWYyZC0xMjFiMjFjODUwODciIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo2MjZhNDA1ZS1iYTlkLTg1NDAtYTcxYi1kNGVjOWM3MTUxNDIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ZjI0NDI5MDctZDViZS00MWVkLWI1YmEtZjllOWM3YzkyYjUzIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChXaW5kb3dzKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjItMTAtMDZUMTM6MTg6NTgrMDI6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIyLTEyLTE0VDExOjMxOjIxKzAxOjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIyLTEyLTE0VDExOjMxOjIxKzAxOjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjY2ZDhlZmNhLTMzNzItNjY0My1iMjhhLTU3Y2QzOGJkNzBhMiIgc3RSZWY6ZG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjkzMmZjNmE4LWYwMjctMTFlNC1iOTc0LWQ5MmNiZGU5ZmNlNiIvPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyYmYwNzYzNC01MTk3LTRlYjYtYmY3Yy1mOGZmOTZkYWJkMmQiIHN0RXZ0OndoZW49IjIwMjItMTEtMDNUMTE6NTc6MzMrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4wIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpmMjQ0MjkwNy1kNWJlLTQxZWQtYjViYS1mOWU5YzdjOTJiNTMiIHN0RXZ0OndoZW49IjIwMjItMTItMTRUMTE6MzE6MjErMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4wIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8cGhvdG9zaG9wOkRvY3VtZW50QW5jZXN0b3JzPiA8cmRmOkJhZz4gPHJkZjpsaT54bXAuZGlkOjc5MDY4MzA0NzNCODEx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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAaMAAAGjCAYAAACBlXr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAHTmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4wLWMwMDAgNzkuMTcxYzI3ZmFiLCAyMDIyLzA4LzE2LTIyOjM1OjQxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo3YWY3MjAyNS0yZDJhLTZjNGEtOWYyZC0xMjFiMjFjODUwODciIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo2MjZhNDA1ZS1iYTlkLTg1NDAtYTcxYi1kNGVjOWM3MTUxNDIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ZjI0NDI5MDctZDViZS00MWVkLWI1YmEtZjllOWM3YzkyYjUzIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChXaW5kb3dzKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjItMTAtMDZUMTM6MTg6NTgrMDI6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIyLTEyLTE0VDExOjMxOjIxKzAxOjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIyLTEyLTE0VDExOjMxOjIxKzAxOjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjY2ZDhlZmNhLTMzNzItNjY0My1iMjhhLTU3Y2QzOGJkNzBhMiIgc3RSZWY6ZG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjkzMmZjNmE4LWYwMjctMTFlNC1iOTc0LWQ5MmNiZGU5ZmNlNiIvPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyYmYwNzYzNC01MTk3LTRlYjYtYmY3Yy1mOGZmOTZkYWJkMmQiIHN0RXZ0OndoZW49IjIwMjItMTEtMDNUMTE6NTc6MzMrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4wIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpmMjQ0MjkwNy1kNWJlLTQxZWQtYjViYS1mOWU5YzdjOTJiNTMiIHN0RXZ0OndoZW49IjIwMjItMTItMTRUMTE6MzE6MjErMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4wIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8cGhvdG9zaG9wOkRvY3VtZW50QW5jZXN0b3JzPiA8cmRmOkJhZz4gPHJkZjpsaT54bXAuZGlkOjc5MDY4MzA0NzNCODEx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" + }, + "89b19028-256b-4025-8872-255358d950e4": { + "name": "Sentry Enterprises CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "4e768f2c-5fab-48b3-b300-220eb487752b": { + "name": "Hideez Key 4 FIDO2 SDK", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAAG0OVFdAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxMjFDOUI2OTVBMDExMUU1QkRBREQwQkJFMUZFRjhGRCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxMjFDOUI2QTVBMDExMUU1QkRBREQwQkJFMUZFRjhGRCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjEyMUM5QjY3NUEwMTExRTVCREFERDBCQkUxRkVGOEZEIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjEyMUM5QjY4NUEwMTExRTVCREFERDBCQkUxRkVGOEZEIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+vr5XIgAAE/9JREFUeNpiDDl6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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAAG0OVFdAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxMjFDOUI2OTVBMDExMUU1QkRBREQwQkJFMUZFRjhGRCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxMjFDOUI2QTVBMDExMUU1QkRBREQwQkJFMUZFRjhGRCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjEyMUM5QjY3NUEwMTExRTVCREFERDBCQkUxRkVGOEZEIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjEyMUM5QjY4NUEwMTExRTVCREFERDBCQkUxRkVGOEZEIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+vr5XIgAAE/9JREFUeNpiDDl6gQEP4ALiBCCehksBEw7x/1CsDdW8D0kMBbBg0QgCAkD8EUncCUo/RlLDiG4AigQOIIuk9i8QM6O7AJ9mdHX/kcPgPwmaUQxhItFmdHAFZAA3EJ8hEBv/ccjrgAyIB2JjMl0ADoNpDBQAFiICiqALYGAdiZb/R3YBI56AwutC9LxwgATbPdHDAOYKJSC+h0dzABC7APFebIHIiJYvCAYsQAAxEigPwoH4CxBvJSUa/xNwESO+AgU5SzOiacLqPSY0zVYEEg+GISxkZGdGpAwGTwfpZJQFcBf8J7M8AOn5x0QgtcGwE7FJGRfYS2q9AAL9BLL1TPRCFR0UYUkPyCANiE8wUVCggoAlshfqSC1MkL0AckUjOWmBCVttQ4TtjLhiASSxBy0NIGMt9DADCCBC5QE6+AzEPGhi36DtCGSwHIijiK1XGIhMzf+hljOiYW40ficQR6LpSya3gYMc5oxEJrkKLOrn4KqimfBYDDOAiYEygO5wkPmquApUEBClMHMR45BbQLwduUB+DcTngdiIgfYAuVZghYWACBB3k9G0QMaTyXDML5ADQqGcZeQURUggh5zmDRM0Hw8YYEJrdFSREI/mBFI7SYX5QijdSoLjT5FYPsCACbYqOYFA/FITnIbS5thqo1QaOwK5kDuFrSScQ2QLl1QgBzWvHz26WAgUFtJA/ASL/B1otj0G7dNKQhv8oKhkJaI4JrqT9BRNIyjE/gCxCp4mzFm0hIYXAAQQqe0BlAYV1KLvQLwfiO/SopuIDHyAeDMJ5ct/YhUSAieghm3GEa/Y4vcfUhOMohD4jyVNyBDb9wGCq4Q63LhCoAGL5Yx4LCeU4v+T4oAlQFxPZhmP7pALhByB7gAzII4mYwQJFzDE0erC6YCTVLScAUf3F28nm9qW4xqgmIovDdDCcnSzs9Ad8J8OlqM7oh5bdUwvwAfN6mAHaA9AU/Azckl4gILUTWnaYWKC9gkotZzcBkwfOf2+51SIgjJYDYvsAC4iNUvgkfMi0owmmJ3IDphHpOYleOS2EWkGO6x2RXZAOJGaY6mYG+YzQdtwlBSrDNDGKTm5YBoLtF33nwqOIBbsw1cbfqFDIeSIzwHcdCwN5ZAdgBycLTS0FDmqH6OHwCcoXU2nyggjCvixNRho5PvPuNIARoOBxi0jvC2iDzTqlhPVL2CERkkZhRYzA/FGfOUGC4GgArm8E4vcGiDexAAZcAR1x02hRbk5joKHkdyuGa7BihAopri0ZCIh4YBwDxFqrUnpTQEEECXjA8QCDSAuhPa4SClpQZPjoNHXRbR0HBOVzdvOgDmEfJ0BMsWF7vkSpJjiBeKXaPKgSnohA/aZH6PBEgAFaA7zwKHuI9STyOMpvWiNAAk0+Vl47D2LZOcvegeAHpLl/TjUvEPzjAAZLZ10NDNW4FDHiuSeB7QMgMVQSy4S4WBhGmTXSCTzFXCokWfAv3iGrACogxoYg61FTWSSpTZ4iGSvH57an2BAkDpECQO8dGq8EwM2M+CfXPgPTb1xpKSAYhyGwUJ9sHgel/uwdWT/E5sCdjNAViqhB9R/hqEDcKWI/4Ra4+vRPG/BQP5Cs8GaInCOEAcyQNapgcBMqMaTDMMDYFs6gREA65AUZzAMTwDy22wouxs5AJC74Ep0cIgntLGE3IpcQadASEVqisMDAHkIgJbDATDPgsYwBdHkwpHk99ApMDxAAWCJpQqkNggjsSB1plHBq4/eIWNiIGFunQKwktwYorI70McTNEEB8B2LwsBBUmjdorJ5LthagvuwKFxFo4YJqWML96joBlMsYnuYcFgCaiFy0iAQDpCg1ovK9h/FItaNbd0WDLylQZJ2ROvju0F7c0oM5C1CI6Xww7aY6Qr6yjlkAEoBwTTO47uhvbn7NLbnAo7IQGkJYusYrRkGrb9XWMQuw7IjcgCAtlxZkTAmMBQAqHMnikVcD1dv8DgD9tmFoRgIU5E6dzhrJGwDIqdwFERDKRDmYmnSb8LmL0JzU9dArSV8AwqDEOwCYldi2yGEBkW1cAwoMA1Szz9G83wdoQgjdW4OucDUHWSeB0WMDJrHmwlpYiHRElgggPrul7DIf4PmtQ0MkK0B1Bw8BQ3P+UILNi1qNbmpMTk6g4H0fYXUBKB1T2RPj1EjL2egNWNraOhZUItRGM0+iuYGWWjgyFYG7JtRWKBtf2doQ0QBqcPFDC3AbkHbIqCS/DY9kg9AAPKuLSSLIAofNaRAJBISI7sQWkSQJUZJmd3wJaxeIogsEIwuhD0I0oNG0UNlRQ9ZUYEQBRKIkRHdyCLyISqQIgsiqMgKoYcSpFDr9J/h36Yzu7P7z6y7fx/8oLOzO3O+ncuZM2fOhuEfIKOYfgW0QEHhPxEBWJmhMCszLoQyammMKPNxDw6el37/jhi2CVgZA2TgG22HpIHzvIvwqlNsOUTaG3rGd+o+kSZgMVUWz/hs9MiL50DQXU6chm3wyI/5btLzO6NGwHyqWI9GXrGTiwrLN0d6C6Wv0HjGOirvXhQIGFEYG2Q0g/tevkA35SskbdMNlURE3VgQsEdzYbSN8hzw+fwPNEDnaKxCz6ayUg0yC+CUle+RZzeY8XgdpJeEU+ZHjbUAuuS9stkCRj2Ev0hv3LS7bz8912ujpA9oz88GAW7N7AdVsMayTnGTynnkkucorU+MEuAm/FZIHsQIC+gOO83lOuoQrabGAO24PWNg/MggvSOLub6DFKljqbSAURdVNSqmsXG0eOLQ4mW4cSPgiiL9KSTc5KKEKlDHt+kNQkAJ8P7w6P1fCtHEflBHtBnyS8AzJg1D5qyHaAPruFZhNdquS8BFJq0LNOMFRQDXqUvIOKNLgOwT/AASxsg4AQdFbnu9w4sA2Vni3e/fcognbjCK2QYvAuTl6HSIN7A7N0ppbSoCjkRIyTEJPHZ2WtJcWQIa0lB4gZ20jhBYIxOQ67iYBekJXEkKU/s5mQBxOhFPfYxA+qJYHtsEAcI5ugz+H8zkZoEFIRXeAX87SmOMvZUhtgCxWvxDQG6IrLeRwPJ8jPE87oJ9L5Rljr83iaVkVUjCo6Niuab9wdYs5HQMLxQtIIymV60pvJcdIlXIDmDZmUy/L7ZQ8NUA96y2UI950v9zMiEZnl2gwnChQe2FrSG0zGlIwESP9YAJBSQIikIgYEImo/isMlxIHkQDXFy8DBGx0Yl8wwUH9cAYNlwPzqbx51sIA5aZfxrwPtOHsbl4Uf1IwAvmwgzDhfcEuMf06TXOsNOHBHAfsqg1XHi5z/wHQxoXBpCA28yFOguF6e5Eo87QZLjsQtUFJIA7HzzZAgHD8G/QTxnoPmfD9N7IpN3xeitIwhcLlRGaJ54TwrCOQ4pWaBLceHLKuRzmBsIWy5VC97drIQivQqeTAK6JbIH0QL3bRUFAl+J6fhoQcMJtnZEpNUkZ12MufI4ifRdHALepWBpzArhQo0NcF0C8VDzkeIwJWOZlFPHaGkPsjanwZxXpvW4EdCtuao4hAZw2O1c1CzgxhUnbnwZv/xPXzTkC+hXKyaGYv/0CNz1ABuebvy8mwnPOXZu9FCEO2UxaewwIkJ27MPzf5SAE/ITkh5EENkZceM65q0RHFVYB4wfIn6V6HVHhxzPCGglri9GFnZ5jRZbsBaniq1/hdQlA1EjL488RE34htQBfwvshAIEuNOsc/+MWdzWM7UnyImqhTxzjlq+NVb+VdwYhwC1utN+hqUvs8+Mg1OQ18ATAJLJPIOk/HOXheCS8Wy4oZi5XBD04iSQ8hITfvjzi4k92XMbzgWh9fk7a2HtHN8KdqTxSVGZBwkyGz/DjoodxQgLtb6RycnQpJD7PMaiRF/NVgPmN15PgYfEx3QWAebPYGhaF3Pe7qNz6VB9kagB7TBXCpvjOouDiM6fGfJdNj+AD1HexkpWgjkKtC/GBAfHp4cOmGbV5evy+NBvMpkXWEpq+pkJyBxi70lsiDI/E3gLzu8MsfgnQ3rmGWlFFcXx56FJkJISamMZNL5mifbCIougq9pKEypIwA82ulN0MNAsq+xJhoWCZ5aOXVpbaA7OXkd6MoqL8EJRmD5MkP5Qa2APLMszfPWt3htOZmT2PM2fm3P2Hg9dzZvbM3mvN7L3WXuu/GsEfUG+QzkMCZZt+BquPo69+TtBFU4tUYiNKOr3+oS91NHmv+hCg8f5OPzssX/qFwTEFvGdYN4h1nqBPVFoR/czUJlqoLcJ5KEaXrgk3S0JKk6xRyvn9taoxvt+z+D2ogz0jgfAPSXlvqL8uspfod3HA2hUH3JvahrlP3iDzxa5ip1MABQuHTz2DyLw4V5KHmWEqTpQK8RBTAHtj+9SJcJt+Z36nlMWXCa/JivAuNXpMf96TnIXjN1oBmJNf9gzQlhQG6C99uk/1CBTi6PUR2lirFqk5n7/ToBlur1JweFz79DQFYDX8hVRyJJKS1vKqnSXlNCeEdaw+3T+keM+8Da71KARP96Py//jSqMDLeEDHYqsE0yEUWgFwUr2uHYXhY2SCtti0m+4RxskqjCzTvPar0rV4FGJZwjbPVovjiL5tejWDAlyvHToktUNPbICL9161WHqpSbcyZ2sXFOIWj1Ky//5+gvYmSaWQ/VVFVADD6vRczPNxTozSweTtcX9WjpGUsEPne6MQSQJLTGrhoiIogClEFyfGeqPa4QwYUbTbmsjfcp9HGeJWLpqtY7s6jwqwTPwL8QUB1+dgqdSR+EWaHyukdq1NW0zRsV6YBwWYqjdzc4zzGAB85Xuk58JUmyVf4NsY5zL21zRCASA2JaB6VYRzWOEO0g4/Kw5e4PA6XcfmqYjnEgm3XWK69eMoAF4zCOROszy+S230Vikz6DoEo0MVIUqm4Ai1lqbXWwFIeVxseewG7chF0txULPXCMoleY4u3x6Z6KABPL5sw51oca+iir3QyTAUbxY5C14AHjvKd/dJSgHado8Kqzb0jdnTZDvFgKIRtwoEoX4qL/KykCnC5hJcE/FyV41Ino0xgAuJsPISEYo6NqwBjxD9/FPwq5Y0dqgn86eSSOV5VRegMOQ5O0NFRFYCk/aByDczvbGN+4+TQcCxVRXgg4Bh2GttsFYAdrtd8GjIFyza4cc8d7lbZrPWR8xu2CoApUR1q9ZZYVqpzaDgmq6y2Vn0/TGpQsVUrAAsLL0kGQRUDdDHoUCyQrXGKlOMnDCAMvThIAarnESJhfnJjWVhQg6h6V3W+9z9e/3GHvia8YFuWOPrfm2hQWOPgOh2q9jIbKjhOdqnCH26ivhJMW82XSuQRYXivVCtALXOCsGkCIj8p8CBAjvu4CjwKiFtkl/OjAvedoJpa9NCdRgHMFEC6kl9SaxHrSJDkYaJvu2II3wzeh1IJ5y4it/75Pt+PVVP/PwUI8uJdULBO87STvpVm/H27Tg0LCzYW40L61K0AJCoG+Yz57biCdBjTZ0Yd258r4a7xvKCfzvdBVkJ/FIBEyuEBBw4MaSgvWJfRfbZL9KCNRoCd26C6d8h8mClZ2jeksfE57yyv+yxZjKbFXFdkiTAafOQ+oKSWQNgCZ0LOOzsq4+uVapjMeUOY8647MLWkwg/bFj5T8s0f+nMDrvl3jscDqtCwUijd+YkIHhKEAxaNXp3jDrPRkWV0Mbugm3I8HjbTIRFeB1EA/P02xDaTctxhsoZmZni9jhyPRYvlw0qU124UgIiezyxOaMv5WoC3wGUZXIdSGB/keBymiA87bBXYI+iuH8KroMuy8ZtyvvAxcXPv1qHt9dr2xzkfg07L4wg2PVzyDNw+i5MmSPpVtuqBcSqsh1Noy+T1TSxAvydZ+kKY8jeLZ/XPbt9ay4vcI8XBbKnk4eEXh5Fjd8i8SO7eOZJOZm/WsC089IJaAeKlicMjuMOyAQpxrhOHPAE63wUWx5GkgxPre6my/2HueMzyYrxaj3djnhu0Hv08aHnsAiP8agUAsFrZVM0iTOxpN+65wWqxS/Jhipvn/aL6pN/EvoIgpEmz3Ng3HIvFf9+/lv/inyAFMPa0bZWUR6R2kRGHbHCDlLO1bTCvlnlcCjh4TQTbe5iTReYYE2EaXuH3UAfNG9epcG0AE+dAJ5PMQLDuFstjIZnyZXAJWzjgWrUpo9hblaCPk03dQZCubX1u+AYD9wVsVo54/56wtAzYJTvRyaiu5p6t8B+S2gXUIysAgPbNxsdMGDmetpOcrFLHGWrG2ZQGmnb0M8em0SgUMeSVEWQQRqsO1x8ZKYOczFIDKfg2Xlpo9uAbfsa24agcQVCZESEcxvIFYTNxBiOc7BKDsHybsi4r9OGLRJIdlyZuqmplGH3rdjVXHOIBHoaw2AOcd0MlJgNpEqJIAkkIKL0j5DjMlclOlpFB7EVYjYOZuujeFfciaVDFUlWTbdOgjSS2H+90MrUGMQjLA35fpGO+POmF0iSLvlVvaqnP79R8W+JkG4onpUyPHyT429O6WD3o4jv1Juf4KMl6J2NfQL1zo890kKrgDbKoG0ju4UYJzqTZowvGbfrh76+lzETWDMAvMlytIj4j9d+BIQvoS9SkrhuyLhxJjZxVkqwcCpm/O6Vcr2+nLoB2q/mzR+pPOY+zC4p76FfgSyZaeoj+PURN4Lig4BWU+y9lJZBGVg5FGeDD7emRRbzlyGh+sREXb2TZOJxJvfVtwHby2z1I6NDwtWrf+zRK+I1WAC/YRBovlUhc5svnRSNXCw6cZSt1LWT6d4UERyf3OAWoxlc6F5Y8g3ahlN2de3Ms7L06rZ3nuW+cZdN1vZI7NEP1cLahiYmDEGG0rrD711HAWCkwkcBBBIHUj0UevF5HjjTDW9YhLv4FMFbB7o//JIUAAAAASUVORK5CYII" + }, + "931327dd-c89b-406c-a81e-ed7058ef36c6": { + "name": "Swissbit iShield FIDO2", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANEAAADMCAIAAABiENH9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAACZvSURBVHhe7Z0HdBzV1YDXDTeasSk2EEwxYMCAAQOGn0DoECdACKYkQCChBEiAEEgghIQEHAihdxLAXbLaFq16772veu99p/eZ1X/fzEpa1gWDZ6WV9t3zHR0dg3Zn3/vmvvtm572xjOHAMbWBncMx1XEg51RVHXUTNc1t9oKKt+LzH43Muzm8dH1Ew6lRnSttgysc7mNiiWOc1DFOEjPboaCvocdX2YdOi+q4OLL+1vDSxyNz347Piy2oqGvtcBME2OL15ttiv84JgtDW0ZFU5no3rfwRW8l1YaVrtpYf/XXtnG1tlmi3JU6yJI9ZUjGhBPQ49HvU6NxtrcdsrT17e8X1u0setxV/kF6eWl7b2dUliqLXngPGvp1jGMbV1BqWVfKcNffG8NIzwhqODu+cv6fPEjlsiSEsNsbi4CyxvI6ACQ307nawqPfBgYjhwyIGjg7rOHN33S1hJX+25UflltW3tHMc53Vo/7EP5wiCKK1v/iy99KGoovU7ylfsbJoT1ocki5eR5vCLjbZYSfTGmFAD+h2ci+WQCeCDnZ0b1nvczsZLdpQ9GlP0ZWZZZVMrRVFek/YT/s5BhgPhPkgqvCuiaM3u+kXhPXOiRixWGgkO2Jlx4bBzoQlpsVLIATDBUMJKzY0aWRLeszas7t49BZ+lFVU0tBw4233DOZ7na5vbPksvuWtP4Sk7XfPC+y1xem6zQ0al9np7TMgDVoB28RJIsiC877Qd1fdFFnyVWdbY1gHzAa9Ve8WkczDvaOvohBruoajCM8PrF0T2I5dhIIcXhZfGgylmb9BQC9pBcS9A8luwp/ec8NrHYgqic8s6u7o1TfO69c2YdM5NkMllrudici/cVroovBvJCzUjJFJsG+bAgCHgiVOwxIlLwjo3bC950ZabUVG3v8Ju0jlXS/t7aeU3hZUu395giRzSpwssrtswBweJMhQ4s2fg2G11m8JLPsmoaGjr2OfVk0nn7Pnlj1qLz9hVNy+8D2kLNRz89H9pDGY/wMQCTSnI+WG9Z++q/Z29JKGocnB42KuXT0w691Zc3vVhJcvCO9BfGjUcTnKYgwfVdrp2Nmb57rabdxe9n5BX09Lu1csnJp17NCL3zG3lh0X0o0oOCbvXi2IwBwacAXPixIXhvWu/Lv1ddF5KdZNXL5+YdO7m8NKjv671VnIwY/V7OQzmYHAw4NyciKFjvqq5LbIsvLLDq5dPTDq3PqJ+/s4O/c/0bxp8XwiDOUjs+vAa416wo/3y6IbPqwe8evnEpHOnRneiL+9tE9807PVyGMy3YtOrOhgno0bPtnW963J79fKJSedW2gb16yOcfrkFO4f5XhjyxHIWp7g6dujNOsarl09MOrfC4bYke9BVFr9XwWC+K04BXFrldG+p5716+cSkc8fEkugeKewc5tBxiuDSCXHE6w0HvCZ8jJNCzoGhfn+PwXxXDOfiqde+zTk9z2HnMIeON8+RrzXs4+4S7BwmAGDnMFMNdg4z1WDnLNEhj1+DBJrQdc5o7qh9ERlKwOedYu1muXOGWPvEVzK//xRNzJkghpg7e4FPZ3xM/3YLKLPWOcMeQyl0QrsnmVAtCrX7AiuxyEYstRNHOohlDnJ5LHWskzreSa2Mo1bFUyfGUyeNc3LC7OGUBPLkBHJVHHVMLLnERs6bypvTZpVz41lqUjj9FziP51qRWwt1t46KJVc4SfBpdQJ1ZjK9LpW+KJ2+PIO+Kou+Npu5IYe9JY/dlM/eVsD+rIC9s5D9eSF7l87molnC3UXsfcXo56Y89rIMGhRcbCe9jebXpIFg9jhnpC5IYxGQzNDvMHAsspFwHp8UT56ZTK1Po6/Mom/IZUCme4q5h8v4Jyr552qEv9QKr9YLbzQK7zQLH7SIH7dKn7VJ/2uXvu6QtnVK27ukHV3STp1ds4Xd3VJkjxTWLcHnfaKS25jJLIvVnYMG9GvVQDAbnDNO0PGsNi+GWGKHTEadlkivT2OuzUGJ6pFy9oUa/vUG4cNWEWSK6JGdA0rqkJI7qpYQaiWl1dJaE6O1sFo7p3VwWhen9fBar6D16fTPLgZFzS17hiRPmVv9uFW8vYA7Po6yROmFh1/bBoIZ7NyEanqJBpXZkXby5HjqglT62hxmcxH3ZCX/Sp3wfou4rVO090sZQ0qxW6ml1VYWyTQieSjFw6tjsmds3ystQyAGBc+ebvmXJdyJ4JxR7Po1ciCY2c7puQ0S2xEOcnUiDWMEFCt/rBbebRHDu2VIY2WE2sRqPWCY7KEVD6d6RM0ja2OqZ8zj/WQhHZDCw7ukX2Dnvh1vekO5DQqRtckUVGmPlMPQKW7vlNKGlGpKhZGRVsAtrNaBopPTdnZK9xZzMIHFzh0QPb0ttKHZ/tVZzOPlHAygzn6lilS7eY2QYcT0QCbD8a0BlesO7NyB8A6mbhhMVzjJ9ek0TPi3NAjWXrmGUqE+w0ntuwZ27tvQnVtgI06MJ6/PYZ538WHdcgWh9gsosXkPFMd3Cezc/jEyXBSxxIautN1RwL7RKKQMyZ28pmDZDiGwc/tHd26pjTw3hX6wlPusVSx2q6OSBwt3iIGd2xdgmw5kuHWp9MNl3LYOyUWp3MFuw43jQIGd2xd6hltsJ85JoUC47Z1SA63i6s2swM59k/EMd5iVWJNM3V/KbdWFk7Bw5gV27pvok4b5VuLkBOpnhewnbWItznBmB3bum+jOHRtH3pjLvNkkwKSBw8KZHdi5cYxRFco4G3lROv0nF582rMAs1XsoOMwL7Nw4unALrMSpiaiMi+iRe4T9bLKN49ACOzeOPqoujyVvyGXebharKFXGOS4wgZ0bJ4qYZyXOS6X/UM0lD8rDEs5xgYoeXtvdJd0X0s7pldzcaGK5k9yUz/6vXWxjNQV/dR+wGBS1iG4J3bMZH7LOeb9UJS5ASY7PGJbZKfx6C95J86DbOQF4WwDGdFnzAtl29tHMaFs7pbuLuJWh6xx85igCjua2fPazNqmZmaKrI6LqoWQPDOL9gtbNo/UQrawG/dHIaPW0WgtQao1ONaVWkZNUzkzgyF36B0kcULY0Cjfnscc69fUQgF+PBIJgc25uDHFeCgVJLnkwsNdHQGdGGRsQwS21jFAzh5X4AdnaK+/plnd3yds65a86pC/apU/bxI9bxY9axA9axPebxfeaxXd13tGBKc5MBI4cPg58ir/WCfeVcOvT6SMdobnWEF0icR9uJ6/PZqCbGxgNRrQABQzZUClmDSu7u6V3W8SXa4Wnq7hHyrkHSzmYxG0u5O4s5G4rYDcVsLfmMbfkMTflMjfmMjfkMNfnMNdlI67V+dHMBC3jzUWf4opM5pwU+vg48jDbXt0ROILFOf2a3LwY9E3XAyWcvU8eCUySg4oNXrmUUHd1SS+5hLuL2auzmXVp9BlJaHU7lDXHx6F1/CucFHzko4FY8igHCWkAOMKOgLNi1rDUTi60oS8Y5/h1R0AJFudQMYHWpV6UTr9Ui77pCsQ1ORAOKrb0YeXfjeIvirkN6fRJ8eQRDhIGdEN6xMTmMUZN7UvErAM+FLT81AypEwSLc+jDE8c5qR/nsR+3Ss2s+cMqCDcketIGlb/XCTBKQlZD+3RAcxu2Gb8Yv4cOE5/arzsCShA5F02cnkj/poyL6ZUHRPOznFv25I6or9UL1+UwK2JJ1OLoRPdper9DwgSIoHAO+jsSijnyojTmZZeQM6JQZl+Wg5G6hlI/bJE25bMnxlFz9XecthM9xAke5xbZSJhSwRy+llJFGAjNC9UzBonT2is/XMatTqTmw9sZI4vfYWCmhul3btyAZbHk7QXsri65mzf5LhJGQRPVNxvEH2Yxi23e2hHntmkjSJybE02cGE89WMo5+xXC7KskfYIW1SP/uow/PZHSc+qUz9Qwvky/c3qSW2Alz0qmf1/FZ4+ogqlrujyesTpKfbtZvD6HRU+QgreDSs7vGDBTSZA4t8ROXpxO/6VWgEHQ1FpuDErD3BHlD9X8eak0lIzG2/kfA2YqmX7nIOtEE0c5yKuymH81Ci7K5JWrbtnj6Jfv12/XQZs1g3B4YJ1egsS5FU7yplzm/RahiTHTOUiZXby2rVPalMeC1pPXR/yOATOVTLNzqKJHP1fq9y990Sa2c2bOWSVtrI5WP2gRr85iFqIZKxYuCAgS506OJzcXsZCQugUznWPVMSgQtzSKl2bQaDt67FwwEATOoSsXqxMoKLnCu+U+U7/1IhVPzojycq1wYRqN3g5fJQkGgsS50xOph0q5qB550FTnRmVP2pDyxxr+3BQKvR12LhiYfucikAdnJFGPlHPWPnnIVOeGJU/ioPx0FXd28vjVYL8DwEw9QeLcmiTq0XLOZrZz8GpxA/KTldyZ2LngIXice6wc3R5srnMwUjv75ScquDXYueBh1uc5cO5J7FxQESRzCKjnjLs1zZ1DDIue+AH5d3hsDSqCxLnT9HlrZI/JdwiPSJ6UQfRl61qYt2LngoQgcW51AvXLEi6sW+4VzHSOkD1Zw8qLLv781PHrc77vjpkWgsS5k+OpzUXctk6pizfzewhG8RS71X/WCxvSGfw9RLAQJM6h71sL2C/axTZTd0QXVU8trb7bLF6VxaBlw+AcvpFp2gkC59DPFU60h6vp95WonrFOXtvaMX5fiXHzHE5100uQOHe0fv/cGwG4fw5Kutg++UH9cZHe5V7Yuellmp0D9Nyz1IbuE/5rnfn3CUvaWOGo+qJLuCiNWYLvEw4GgsS5BVby7GTqmWo+Z0QBS8yNVlb7b7t0RwF7AtpNkrBE4FQ3rUy/c9D9+rqvHySQD5dxCQOy6Quq3bInY1h+ycXD7BUtiQDncFU3jQSJc8DyWPLOQja8R+oTTV7fKmsemElE9kiPlHNnJdOL4U1RtjN2vhn3z8D3wDABYvqdA6CzI4mldrRR+idtYiOrSqbuIQyvJWroosn/OtDeuecmU/Behuhe5wwmzMPyBZTgcW6+lbw0g/5Hg1BMKGwANnWlFU8lqX7VIT1ZyV2dzZyeSB3npGDuMt84gCg953kxUuCBmfifTWJC/e/KxHli4NuwwUnQOOeGkg6mEU9V8vED8ojp8wg92zGKp5HR4gbkt5rFx8r5W/PYi9OY05B85JEOYqENPSMA7f5ndJ5f184IZoR2weMcNNmqOOrnhezXHVKnqau/fAPmJ0OSVkGq9j75wxbpLy7hsQpucxHaw/WH2cylGcyFacw5KdSaZGp1Itp588R4amUcdXwcCV4eq7PCDKB4BY7RWaZztANxlA7a01Pf1hPthmlDu2EuAWzEYhuxyEYstKJnPS6wEpCh58Xs6ySZyMTBaWFQOAfojQXNfWUm83qDAINg4LZMhxfmVM+g4GlitBK3mjIow/QCxtz3W8QtDcLLtcJz1fyTFdyvyzgo/u4u9u4t/JN89sd57C157M25iJty0SbD35sbc5nrgRzm2hzvHr9XZzE/zGKuyqSvzKQ3ZtKXZ9JQaVyaTl+SRl+URq9Poy9Io85Ppc5NodYmU2clUWckUacmUj9IoFbp+9GCwWDqIjuy0OvZxHiNnds30C5RcPqS0JS/reDAA0bxvnVAA8yG2nFIRPvz19FoA/XcETV1SHH2y9G9Uli3tL0L6ahvoC591Cp92Cp90AJ2Su+1iIfCu/r+5f9pEt9qEv/dKL7ZKP6rUdzSKML59s8G4dUG4e/1wit1wl9rBcjEL7qEP7mEF2r4P1bzz1bxv6+CU4J/vIL/TTn3EJwYpdzdRdwdaLtt9ppsZkM6ytM/SEQWLraR3q0Lgkq+IHIuGl2lg1Hmp/looWsnrymBGmD3ESCfqKH91EnZMyqBhZ5+QevhtS5ea9cfF9HCaJAXoRxs0Kk/NOpohItC1FBatU4VpVUSSgWhlBNKGaGUEkqJWyl2K4WjSsGokj+q5I0q2SNK5rCSPqTAiZE0KEPt60CnhwynB9QkH7WCteLz1fwDpdyNOcy6VBpSILq5YUI7v2afFoLFOQC0i0RlyoZ0+tU6AVp5alJdUIXHg9D0n8YvcDIYD+UBZP3EEAAVygN0hsBknFTQSTIgoNOjSX/WBRgZ3i1DKn22mr+zkIUx+uR4VBRO5rzpTXhB5BwAJUg0cUoC2oguvFvqMXVN/6wPaCxZ84CIoGA3r9VSWsaQsqtLghP43mKYodMwd0E7xE/MLXxbfioJOudgJmGHmQT9j3oeBpfAzSRCIXjV0yd4ikbVHZ0SlIMwAVqThHaL9ya86dIuuJzTM/989N0rdX8JC2UK1FWmfiURikHLHihJYcD9oEV8qJS7MI3+xq2EU29ecDlnNAF6tCF5RQb9Wr0AFTRj9lf+oRlgXi2tQsUC094rMpnlsRQaZw3zfLtgCggu5wwiiTlRaIXEPUXs1k6pPQDPJwnNgPIYGtPep/y5Rrgqa/whGVOf7YLROb0hINVdkEo/U8UnD8im72odsgET4V7BkzQgv+QS/i+LWeaYjtouGJ3TTztoi6NjyetymP80iZWkyuHZhEkB7TggaHH98nM1/IZ0+oiJ2s6vFwJHMDpnoLfC6gTqnmLuqw6pkdawdWYFTMs6Oc3ah3ZyOTeFWmDFec5AP/kWW8lzUujHKjiYw/aJHlzZmRWyNtbKaXAy/7yQPSmemg/aTdkIG7zO6SMsNMThdnJDBv18jZA0qPSL+MqJacGraMH5lgbx2mzG++SMqRlhg9c5AJzTrxIviyWh4H25TkgdUoYDcGtdyMaw5EkYUH5fya9NphfE6NpNQaoLaucAPdXBfGKFk7w2h3m1ns8YkofMvXU9hEPxjDWz2udt0o/z2BVOfRuhKdAu2J0DjIZATxQmr82m/1bHJw8qfYLH3GWwIRus6skYVp6t4tel0gttxiM0AryT0AxwDtCdmxtDHOtEz9t8ySXAVB9mXpLJa/5DMeDMbWS0T9qkn+azx8VRqKkjsXPAeKqDUxAG2SsyGDgvw7slF6XSgXhuf4iFW/LA0PFMFb82hda318DOGUBb6OZBtjvKTq5Pox4uZT9rE4tG1SF8DeXQAoaLOkp7r1m8JptdYqzCNFrbrwvMYsY4Z6BrByfiYhtxZhJ1RyHzz3rB2qu4KA3MwzcDfM/wjI2Inuhe+d5ibmU8Nc8QDjs3CbSFfmvnQitxYjx5RSbzSBn/QYsEowNMwQgZzy2+T4jaWI7+xFGYSSwO9E5CM885wDgL0RDgXmonz0qib81jn60WPm0T4/qVckJt57RhEd0xizPfQQa0Uy2t/qdJvC4n8NeHZ6RzgOEc0g4SHlp2em4KfWMu85syDkbbrR1S4oBSSqgtrDYgeCjFI6joWhS6rIct3E9089rOLum+YvRVGGrYwM0kZqpzExjmRaGlxUc5yDMSqf/LZDYXsk9X8a81iJ+2SWHdsrNfTh9C66ZKCaWKVGoptHyrlUPrC6GhewWtX/AMiJ5B0QPZcURC6wlgKgfDNEDqUDq0vubFF+b7AjkY4FQEr6I1NTC6SRpaZaPqS2+mPuCDJw0q0G5nJ9PoBifs3H6ZSHhANFrgDuatiiPPSqY2ZDDX5bJ3FHAPlHJPVPDP1/Cv1AlbGoS3m8QPW8Uv2tHiPDiz9/RIUD7b+uTYfjmhX04akFMG0WK+zGElWyd3WMkfUYGCUUShG1H0fSkm1BJChRxcRqhQBlQSahWpuigVhrYGGiVmOBN6eG1Q1NyyxqqeKbsWxCpjcGD/bBA2ZNCB/cp/xjvny4R8envNs6K9nlbEUifHUzDJPT+NujSDviqLvj6HuTWPua2AvauQva8YGfnrMu7RcvCS+10l90wV91w1/3w1/2cX/6KLf8nFv+ziX6kV/gbUIV6tn+Qf3x3oVGjr1xuEfzUIbzQKbzYKbzWJ7zSjhdYftYqft0tftkvbO6WIHsnWDxMjGdIzJOZeXoPsGND8B1kW0v/HbRKUdIvt2LmDZCLnod060AYo6F+i0SU9yH+LbcQRdrQnCNTIUP8d70Tp8KQEtP3C6kTqtERqTRIF2XFtMnVOCnVeCrUuFe3VcEEqdWEqtT6NuQhIR1ysc8n3BbIvcGkGc1kGc3kmszGTgak31AM/zGZ+lMPckMvcnMv8JJ+5E86HEhbOBEjPUNpH9sglbnVA1AI3K9JvIdZ2dcu3F7DLYBphNGYgtJtVzvmhC+e18CAx/sRAfxEoEyexIuYeGpB9ARi8fIFTYqGNWGQnljqIwx1QHhDLnST0yqmJ1Pkp9HXZzEOl3L8aRShMmxgtcLdMQ/3q6JcfKGFPjAvkPeuz2TlgQiBfsSINjM2LDpKJvzIV3+PxHh6kZwN02PNj0F2ry2ORfJAIYejf0SXV0SoXmHQHUxkoZKHGOCOJ8t7aBPg16aEzy537fkyYOl14/dMZ1xFq03Up1MNl3K4uqYVRAzG3ULSxglHlhRr0qCpw3XsAfo1z6GDngot9ygfaod2PiSVWYl0KDbOc+H4ZajvV7NsI4eXKSRXmSZelM1D7YudCG8O/CHSn/jVZzL8aA7XavIZSYTZ9tb4METsX8oB2e5AEqxOoB0q4Pd1Sr6nP4zOigUY3mNyYM77iGlKs32EcOti5mYRe2x2lP6dqS6NQTZr8zCCIFkb7tFX8SR5zAvQ4dg6DJIhCk1mYVz5ZycEc0/TrJh2c9mW7dGcBc2Lg7hnGzs0kjKouGi0N2VzE6o/1Nvn5Ld28tr1TuqeIPTlw3/Rj52YY+ni31E7elMt80S61sKps6uy1V9DCuuVflnCnJIw7B6L7HcMhgp2bYYABkWiz76symbebxCpS5U0dXvsELaJHfrCUg5kKdg6jg5xzz40hNqTR/6gTCkcV2tQrJv2iFtUrP1TGnWo4F4Gdw+jOzYkhLkilX3IJWcMKYeo3ElAgRvfKD5dxpyVi5zAGunPw89wU6rlqPnVQHjV1c75J55L0Zf3YOcyEc2uTqWeq+CT0bDRTx1ZhfGw1nMP1HGbCg7OTqd9X8on98rDZzkX2yL+COQSMrdg5DMLwIIZYm0I9XcUnmp3nYN66p1t+AF8rwUwy7hzUc3+o5lPMrud6eW1Xl3xfMfcD7BzGi+4czFvPT6P/7BIyzJ63dnHatk5pc1EgVxxi52YYunNzY4iL0+lX6oS8UcXcXYLaWe2/7dLthexK/H0rxgtyjphvJTdmMW80ieWkyfepNzPaR63irfnMcdDjyLm9DuDQwc7NMHTnFtnJa3OZj1vFBkaVTL2fqY7W3m4Sr8tlluP75zAIEA480DdYhuFvZ5fUw2tmKucZqyLV1xuEK7OYyWeC+R3DoYOdm0nozs2JJk5OIB8qY+MHZNLUYg70LXGrf3EJF6cxSwO3OxN2biahS7DYRl6YCpNWvtCtKKYOrJK+I9gzVfw5yfRCY/sI7FxIo1dyIAFU95vy2E9axWbW5P2UaWUscVD5TRm/OoGaPz6O+x/GoYOdmxkYBkSgfTDOTaGfruJgYDX3URmQMIdET1SvvLmYA63nGMLB+/odyaGDnQteoL8NoO8hw+mPGD3OSd1RwP63XWygNcHUgVX1jLVz2pcd0i357OSD57BzoYVhmyFcBPplmQM9D+jVeiFvBN2qae5KCDC4mlLfbhbhLQ6z6W8aCOGAoHNu4szGGEQT86KJxVZiZTx1TTYDU4eEAblPMH9lK0yBs0eUl1zC+Wn0HJhAhIpzhm2RvpvTzAoi9sL3P038DvioBiywov3LToonL0qjf17EQiclDsjdvCabrxx6qKu1V36kXL9bE/oCnPPtGhMJKufmWgnI6ovsxBI7OauwTbLUruMgD3eQUDYd6SCPiiWXxZLL48hj48gT4qkT49F+eGuSqHNTqEvS6R9lM/cUsy+4eKi0CtzKINqmxNsp5kYLo33WJv20gD0uTt98LhScg3x+pIM4JZG6II2+LJO+QufyWcHGDMQVmcyVOsYOhzBQXmdscpjHbMpnbi9k7ypif1HC/aqUe7yCf7oK7fK5pUH4qFUM75Eyh5VGViMDtuMheFxGqH+vEy7PYA53GKONfweZRrA4p2/FekoiWrb5RCX3ch1v7IQKrTALeFUHbexajzZ2BZPeaBT+3SS80yy+3yp+3Cp+3iZCGtvZJUX0yLZeOWFAyRhWikYVF4WeOzAkeTgVaRG4QFfmBpRHyvjTE+nDAnc12CAonNPLOKhdLkyj4fze1S1BMVvsRhSOKgUzn0K3UoRQi93eDazLCbWCRLtX14zvXt3MaqBXF6/B/GBIRFu2c6pHmZKHhkL2hPf9ulPalM8eHRvIHTYNgsU5fZ0wzNL/3SRCkoc5lKTvXQ+IswvjQ8EkwAD62wDSmKYDlk2FaD5BKx44JV6tFy7JoNGz+QMqHBBUzkGJA+VLCxuAWRmO/UcvWrsvQR0JxbQx5vh3kLkElXM/zGLeaxHraFU29Qo7jgMEDN/lpAIl5tVZzNGBu3/Jl6By7qosBspqKHHM3YMDx/4CWnlE0qx9KMmtTqAWQF9g53AENFgVkpz6ZqNwVRa9dOI5JIBfB5kLdi6Uo5NDKwu9O3/pvYCdwxHAIGUtY0h5oUa4JJ05PHC7pO8Ndi40Q1DRXSQwY7sxlznWSaLH8BsDq1/XBALsXAiGonmaGG17l3R/CXdqIjUvZvzZaH79EiCwc6EWigdtGhzVI/+2gj8vldYfYag759cpgQM7F1KhjY318J64fvm5an5DBsxV9VtIDPw6JXBg50InJM3TyWuOfhnmDVdk0gF80s2Bwc6FSIjaWAurRvdKz1bzGzOZZbHj8wbsHHbO9IACzi15Kgh1W6f0ZAV3STo9+R0XtDzg1x2BBjs3u0PW0IXftCHlP03iPcXsulTqCIe3wafBNgPs3KwMaD9G8cD8tMSt7uqS/+QSbsplTkmgFtqmL71NgJ2bfSGoyLZitxreJb1aL9xTzF2cTh8XR86Hpp4YUn3bf4oJKueuzmI+aEH7W2lTcn/s7Ag4PSUNfVtPyJ5eQaun1ZwRZXeXBD36QAm3MZNeFU+i9AaNDLb5tfy0EFTOXZPFfNgiNjEmb8Mx+0LT97OB0XNU8vTwWiOjlRBq8qC8u1t6u1l4robfXMRemcmclkgd6dDvNZ9YxejX8tNCUDm3MYN5rV6A07RP0NyyBxiWZjUiYkhnUGdAQPTp9Argk6eL98AkoJ3TWlmtiUVpzEWp5SRaZpE+JDv75bBu6dM2cUuj8Ew1f28xe002vTaFPs4Jpdv49d5pH0z9CArnAP2xpOekUL8u4z5oFaN65dh+hK1vNmPtRcT0ytG9clQPIqJb2tMtQbra1SXt6JS2dkpfdUj/bRc/axM/ahHfaxFh+vl6g/hKHf98Df9UJfdQGXdXEXtzHntlFnNeKg2zhGWxpHfhFsxMjfRmmOfX4NNIsDgXTcyLIeA4Ls9k7ixiHy7jHi1HgIKzGPiYBqDOr0q5B0u5+0vYX5awkK7uLmLvKmR/VsDeVsD+JJ+9JY+9MYe9Npu9KpO5LINZn0avTaZh6FyVQEHXHO5AFdtcvRm9BKFqEwSLczHEnBhisZ04No48JZE6I4k6MxmxJmk2Ax/T4HQgkQKHTk1Ei/ghXf0ggTpJX9O/Mo6C7jnOSa7Ql/sf5SAPt5OLbGhYgBbz6mWkNC/BN5j6ETzOGUA7AnDKhhTGp0ZEfwNvrtonIJaB378De7VqcBFczkF7obNWP1/99pIJafRMNiHZBDNFMj+CLc9NtiPmwPi12wwi6JzDzHqwc5ipBjuHmWqwc5ip5qCdo7BzGHMwnIunXmsQvXr5hI9zsaQlZcwSy/v/PQbzXQHnUiDPEa8f2LkVDrcl2YOdw5gAjJZJnlXO0S31vFcvn5h0bqV1AOnp4CxWymIl/V8FgzkYwBzwJ5YD7VY7Bt+spb16+cSkc6dFdViiRi02BgF/5vdaGMzBYKMsNtpipy2RI2utne/WjHr18olJ5y7aUzd/Rzv6M0h18Ge+L4TBHCRgm4O1RLsP29G2Mar+i+p+r14+MencreEly752zYkatsRJFjvj/1oYzMHgYCxx4pyIwRVf19wRWRpR1eHVyycmnfttZO7Z28oWRvRb4iWkKi7pMN8VcAbMiRMXhfeet7X0mZi8tOpmr14+MencO/F5N4aVHBPWgZIc/CUaXrF2mIMGhANndHNW7G79cVjRR4n5rpZ2r14+Memcs6Dyt7aSs3bXzQ/v9QqLZxKYg8cQLoZcsLvn3N2uZxwlySVVQ8MjXr18YtK5utaOD9PLbw0rWbGtzhIxaImX0WQCpzrMQUGiK7tQlYX3H7+t9rbw4i+yKps7uiRJ8urlE5POESSZVl77J2vuJTtKl4R3w6iMXgVfq8N8K2AIeOIULHHC4WEdG3eW/NWem1NVT9P7uDgHMemcqqodnV2ROaWPxhSuDas9LKIPZUsnmKdfOsHmYfbGqOH0K8AWG7VwT8+6sJqnbIX2/PKenl7PfpbNTzoHIQhCQ2v7l5ll90YUnLajekF4H7pugi6dQGGIazvMXoAV4AYY4hQXhveu2V75YFTB9uyylo4uUdzHN61GfMM5CJZlKxpaPk0pui+ycO3uuqXh3XMjh1HmhNoOQPJBzgP/cNoLSYxh1JguGEpYqXlRw4eHda0Lq30wsvDLjOKa5jZIXl6f9hX+zkFQFFXZ2PpVZvmj0YUbdpQev7NxblgvuoACFSIYDW+Dh9qQBQ2mDBpMjQHQxszb3b1yR8PGnaVPWgu3Z5e7WtoZhvGatJ/Yh3MQHMc1tLRH55a9aM/btKf07LC65WHth4X3WvYMWqLd3neFIRyqPUwIIegZhwEH5kQMwmC6fHfbObtrb9sDk4Y8qOGa2zsPnOGM2LdzEDAed3V3p1fWfZxZ8ZSj5Oaw4rVbS5d/7Zq7tQXdCgBHkDyG7vHEhA7Q49DvkcPzt7Ucu9V13rayW8OKn3aUfJZZkVVV39PTs88rI3vHfp2D0DSNJMn61vb4oor3Ewueis77aUTZZVENZ1u7TnEMrnK6T3ASJ+hr0DGzHQr6Gnp8tWNwrbVzY1T97RGlT8fkf5iUn1Rc2dTeCfUY2OL15tviQM4ZIcvy4OBgdVNrSnVzWGX759UD77rcb9YxW+r514AG9ChwzOynnoce/3cd827N6BfV/Xuq2tNqml3NrUNDQ4qieF05uPh253DgMDewczimNsbG/h+9P7+KfKO+RgAAAABJRU5ErkJggg==", + "icon_dark": "data:image/png;base64,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" + }, + "8d1b1fcb-3c76-49a9-9129-5515b346aa02": { + "name": "IDEMIA ID-ONE Card", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAACXBIWXMAAC4jAAAuIwF4pT92AAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAthJREFUeNrslt9Lk1EYx7/vNte0vXOk7yS7qyWBYvnjIktGU0vDCwktV4KXpv3wB/4BBiIa/QC1wjkVUxNsUuuuzd1k6iBLCxIFzcDXOTZwY8r2sr1rp4uXZuoggryJfS8eeL6c53w45+E5HIoQgoOUCAesGCAGiAEAyX6LZdn19XWGYdRq9T8gkN1qa20VDlVZcZUQYpuZKS0tHTca9ywz6Hurq6s/zs6SP2kXwGI2AzjKqHQ63ft3k4SQpoYGAMWFRXvKLmoLAAwODPwdoLdHD2BkaOh3843J5HK59pTV1dwE8Gp8fP+OS4tL5rfmH6GQkO70oLuzc2jwuSop2dBrOCynk5KO9PX3Z2ZkMCkpqyvfGIYBcL+9w2qdKCoqCgQCAHieF2ofP3xkMr1W0IraulptQYHP7wNF7e2BNl8DIO34CQANd+u7u7oASEABqKupJYRU6a4DoGXxqaoUpZwWA9aJCUJI4QUtgFPqkwnSQwD69ProVxQMBtvb2iiKetDRwfN8KBTiOO7Zk6cA+noNLMsCyMo8zfn9HMflnMkCsLS4OD01DUB39RohxOl0yhMS4iiR3W6PbLszB3FxcbRCQQhRJCZKJBKxWCyTyeRyGoBUKv0y/xmATlcpi4+XyWQajQaAz+ebmpwEUF5RDkClUhVqC3gSnp+biz4HnN8PwO/3R5xAgMvNzk5mkkWUCMDq6nfBdzg2BDCtUABwOl2/fIdAig4IBoORKIjneQVNb3m3ii+XiEHp+wzpGelut/ul0QggEAiUXSm7def2vZaWtLS0hYWvH+Y+5Z/Ny8nNjf5USCSSSIw44XDY4dhQKpXDw8NiiqpvbBwdeVF1owoAu7aWmnrM0KPf3t6+VFLc1Nx8Pu/c6NiYSCSKPsket2d5ednj8UQcr9drX7e73ZtCyrJrVqs1HA4TQpZXVrxer+C7N90Wi8Vms+0fCyr2q4gBYoD/APBzAI6VNqGQPUqnAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,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" + }, + "454e5346-4944-4ffd-6c93-8e9267193e9a": { + "name": "Ensurity ThinC", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHoAAACoCAYAAAAvr/rAAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAG7tJREFUeNrsXQmUXFWZ/u99r15VdVWv6e7shCwQoixBRAyDIjCiDMIojqODx8OMo6OeMagzOuNRORJAnUEcZ0ZI4IAOyL4GSAyMHDiDJCogoECTrROSkK2TTi/VVd21vPfu/P9bku7Ke69rebV1vwv3VLrq1a177/f/3/3//25s46ZNUIU0kzH2OL42Yc5BkCiFMe8FIS6vRp/IVWqUgnk55kiA74TUjZlV44d4lRokMGcDXI9LmWr9EA/6enqkAOgA6CBNpSTXST1o/NanYP/qVh8rAdAA29HF+Di+JqYgw6jA2DJ8fQhzx3QH+gh2xmbDMhdiKrJmyso1BboeNEiy8tRLjNnKVHOF4nXYMVMJ5MDqntJg12EbeNBR06PuPOiw6VFnue47zs0SL6RTi7XiCwXKqdw6F0y5YbSkFNdrfOeXIzCTldsA7DN9QqD5YNDffgDUIEOM3BC19CuQMpXctymn0VMzWhYAHaTpSN3u2kwcTBMhs+D49VYhzHdjTpb4q1TmJ61yx8+o0TKoVzD/LgC6eoli4zdgfpfL50+VAfQpmG92+Ww15t+DuTQqoO5q6DrmEY/P1TLKTnt8lmzQ/grG6MAYC1IAdJACoKuZYhVql5eBGm1Y94q1tlg9w0AXtJrHit0K9CxG6tr2eMMCVHcAqpwtLmOYtzgYdFTuvnp3R1k8DkKSgCOGbFwkUE6vvs1wSlO6BlElBE2yDJqmg4hGgX/m0wDZLHZbjbZLuc9eafju31tulnDwsVOexXr/6mtY4NkO5XKsS8bNtWK1DK9ylPdIBFgoBLnH1gLrOwTDuQzoun60XnL6zrsMtRhQc9ARa4I4AqyqKggEXH/xRYBLLgF+wYfMDs/V1f64sQqVqzeEG0V4IEYQDgMcPAi5//wvA2j1T68DT43B0GA/qMjK7Ch1t7QYf0haDlhTkyEZqNLYXGxvby+wO26H3L33gvTd7wI/aQnqSqq6kwNec9LTOSkKivoYiJ07Qb1lNYjeHSBCCDxHkmtrBa7nEFP1qEZzzw5G7TY6eXAQ1GtXgdi+HaCzc1rPAtWctgmPWMyga+1nN4O+8msABw4AxGOmknJetIV5DHCih5ERgBt/DGzZMhB/exVAc3P1qNwPraYOQCPFYCsqj/72of6s2uyGIIsHHgTx6qugv/kG8BkdJvtO0j+8YCki7R44AvDMM8B+9CODNgwBqHdapc4hoSSjcs8e4Fd/DdhddwEMDZkaQLkRhgYSTLSo4f77Af7nToC33jI1u8C686IoAwd7aG0F2LoV2PU3mON1pIp728mGKKa+JJyY2caNwFavBr7yamMYgl+jsH7xHwDWPg6wbRtAV6cr5TmWS8yA/UDGT9WEhNqOthK7514T8GL6ouTAAv4QQ7D5DRbY1OBKayUaH2ztWlOwyNokYNwyAdHWBuw3LwC78UZgN/wA2Esvm0JKn9H3yR25/XZgN/0E2E9+CpBImJ3nVa6tVYcPG2WStVsVQUfNZffdD+ze+8zfL2G4KG2akqSYfnDLNuDXXw/6NdeY2kP06PeYRcDgEMFWrwF4+ilgqI3iik8AnHuuOXw4PT8wAJyeR38SBgcAWlqO1Xt8am83KfzJJ4HhmAennwY62R9pl4MIUKA5GkAGC7zzDsCuXSBWXYuM0GXWxe+2k2Chy8vuRbq+5x6TqkvVlQ2z5hpq3Y/uVRtKdCdKaBYNFoqQMew0BgJUXYBgHELcjKCZ/1uNQo0WS5eCuOZ7JvjODZ6HRksPvrY41AFVDc4Fp6lFIxCAArRmDbB1603LEn18QyMlyVsQSehszS8kkWFG9Z6MnWxhpueorbNmgbgWwZ7ZDTA66tT2GVYUb7ZDae9gXZeA07EfVA71J2oxQ8o+yjbYNp0CWpyBhn5yfkRTVzV8TIK+gX60Owtxr4qhcZRwTlSWTJr+nV+JNJlA3rABabfFbCiVbzXYNdt+Ji+ieQQcCYZXubadYj9HtI2uDSOg+/pMVvNxTKbxmN13n2lM8vKgKh9o26/bsgX4ddebUl6ukULaSkbUbbcBrFtXlHVZ9USatn8/gr3KGLsNT8QH65rd/4BhfBnl+9B2f2avbLBJs8kaT6dLd1sIZDKUbkWQn1xnSnO9hyKp7WiYse+jZvf3Hws0legnAxpeBsj0b5/GfX+nKaliW7eZmk3WeLHSbQdnkK7hV78yjahGiMLZNG6DfRBpvClamsDYdE3jM/cPHv/no8lgIteLNNum8UJBJk1ecyuwJ9fXvyY7JdJkovFVSON9RdC4ZXiZLtS9ZVnX1QPadr2IxlddZ4JNhpEbldmzMORG3GqNyc3xxpzIsLWSDLTvf//YmO3VdvqchJrG5DJdqOoCbTeAjAiKoBGVjYzoBpXR+JufSQgURYdbVjcWXU8WkSOwv3cNhY0F/q07tj0c1tEfF/CLXxwLhnDeQECPd73eeAPY88/LqLW6MaGQnznnSFcSe/iRY4GNqZDCEZPGN/5WRlC5S9t19txzErvzlyaLVVDACw2YoF/O58kMzpI4p0Xz88E8qXfys8E0LLi5eQF0dV0Aui5PXKBhRF6S8Paup7CRSQJ9ygBNzdS1LAr7fGz7hfiGchyFM55Fel8PyeQQCoNUoGKOIDa7dCF6NKG/isD04b/FZAGTyUKgKJbsYkXi38EvnMOghC0KVH+KIw8MuOo9UvengE+xBanMajtN79JEinNS0Fi9AqTiDmUi8CTKiDuCvA4V6N80IV4SHhsXZNeyOF+B2vvfWN+zxgtpSYmMLVmGaZkq3HbO2GVcki5DrNbldP1biPTWgsZoZAD8Dr9GlqRN40EOUn0nxOwyRZJeV0Khv2aTaTTyPGec3YS8//Wg6xrN2Df4VgnJ0oMqgy5k9ltoFaht9hwFGrmeKbK8Ckn768FivMb27kKSfHNbvPlQYnT0YVrfLWyrmz7lYeXClnjTs4h4QXZGWWN2kEq27wrpd2sKOZ3MZt6dVdWd9JeclSUIM9bcGo3ewYS7v8SOaf5G9ACfxL93MfOqgGBJaHUSLQFsQp1chl75JxHMU91AF/gfWuaRqCzfkVPVP0da12Uy1WVF+SvO2ULNhbJJ/VVdf1kV+pe5pv8JrUgtQLdGrjnFNzT9h4Lx8xGX22XOFzjhJgyrWrogpigrhKpt4lGJK4osfVu4gYw5p2l3j2ra+Vl00BFzLejympN4VtW0Z4az2bMRl99yl4gaaXaIS/8qSbyZRyS+CKn7ZCfKJqd8TNOe7Usmv4gW3BgLWLp+3CliWSEO70sk/lLVtT7X9eUczgfGz+E64+c5xVitKFg6lcuuRJ7PILWbO/SCPq4fo4zw4Lw/oWlfcseFtQiJXchDknSu7kDbJCFpTV2fUnNbkewhlctBIpOBHFnoQV/XFOSspkMGMxMMWsMR0FV9A2r1djetlhlfgErKFrs5ZKqmP8GB6+GQAppOkxs6JFQNRrLBXWW1NMgymkC7CcdfMrgQFxn/FDrcJ9zjHzSb5HxygDA3Um9pylshYiwtDfq7JppM1nVKU62LKdgEVzgj9C1xJoHuArbsHmER0CRJuSaXmRURBEyqngis1pDsFiRJe2i09zSlKEDKArCrZ3xJ3gsTmJhESMquRAB29cbnkt0xPyUuSJXT5rL97nqrUJCO9aeffcrrUfqC5H/ilZDEINVfH/JGqWhA13UIdAD2NKDuAOz67Cve6A0IQK4ToAOwazMm56eqrqrnQkDIPtCtgikrSQVHkahOCm1B8qoTnRvCOWS5cYILhLXCFtlkrHootOyWVnxZvyGsOlYzVQ1oGRtKjdvS2QmSrpdUhm50Fphrld12ouLny/qPGACmjb1j7imWy8FgJAKbO2dg/dxFQ0OAOtJpWDg0ZLShh07rMxZhMFdV5Vjesv5+o9072tthOBwGiWb+6CQtTYeTBwYmrV/DAc2tDWBPLVkMPV1dRuOLTTQzk8lmjA1kYUUBt5ka6sje9sOwaHAIzuzrgzFZNrRVjKNLqg/V4Tfz58Oe1hbY2tEBisfMj8oZdI2OwtJD/ZCWJXilu5tWdrjuCaQ6kDC/g2WHENSerk4YjEaMuWNhCcGlvTvgDKxfukpblSr+K5LVgY8vPdno0OZstqTgPAHLjZ2CSLWcg9eUHAnT221t8OrsWXDR27sMkGx6VjH3IKu8jJ/1W6fvtUxyPhp9kgwpsGneXOPA+gjWg43bBeGW/jB7ttFWovpYNjeBITag0FPByw8g2CG54hNDcqXpmjrXBjlK54BXQXrpd2hM7YvF4P5T3w1zRpJw/u7dkEFtfGrRIkOLqLMVa6wVkxhFwhLYqLWduNAUUVVX4SdB2bB4sbGzwmaehgSa6JGOBX8CQd5mgVxVi1aYS20oHYzH4JennWq8R+BS3XiNtx3ZRiANZyRkZ1hgs0YCmhpBnbr2lKWwvYqa7FWfSJUFrRjWIxqn/jnTGrMr0Ve8EhUnqXwcQSZNjtQY5HpPtmH4NIL92syZECn9DHGvU266fdVo2314vEZ03chgc4vGKS0vjcbpctRvwcT7QJglAH+U/awsWRa2JkcDTS6JDQ2wsR+X9x1GsIsIqjDYgV+8qaLULVl0vdayrgO6LoPGMT+x4AR4sb0VmnxkRNkPkMkHXRvQtW/DH4VMH6BbAVBb3j88DCkfImi8XAk0/eSTDOu6KQDZH9cQzPj4Y3Nnw0sd7dCklb+BlZcjeVQh00+eEYzJFQCb+vhRBPvl9jaIIdjlaHVJ1G1HvMbTdQByZWic4uOPzp1DZxjAOQODMOpgoFHf09geJhzcNtqVImk0g7N+8eKjwZAgVdZAC2MmGqfOX3FkwLphddwFo0KHw7Em2DRznuuEkVzKDw9EIrClcwbEA02unuuF/b5+Zjf0treDFFYmTNMSJgl8b29rq+sUcFFAC4u2NyxeFLhQNaBxMsm2tLVANBI9bnKFwI57bGcuCmgCebs1iR4sD6oBjQNNeermlKvTpIzHVGtRVjfNq74yayakQiFj0iJIjSUkBSXykf80cybsxnEgEhhgUxNo4v9USIadba3GhH1A21MUaDIEDsZi8MfubohowTFjDWm1F+NWhShQcuytVszXWQ6dVkFBpIVd/84Y6+XmgrxFaIh8D98bznsujplmbzaX+FuXYb4Uc/6Fl3Q1379gTuW9fybmr2EezLeXrH7Zn/f8HMyrYOI0olOidjyEbXymyPrT/UvXg3GYPuTG1YeuG/q/ciY1qICrqySQD2HutY5XOhHz37k894Qb0PTdSdZ7fQTzl1w+u9YB6NMxX+Xy/B0OQM/C/IVCGhsKhb4QDodPw/q+OX4BonHsI01wOLeFAP5nlyI7y5nUIC3WqwT0+OtfcwU+5wi2R0p4fObUzjGP553qWJAFa9xlgvVsbW19TJbl5pGREaCcSCSM3N/fD5lMxqkthPyoS7HD0+7+A7uDRB27hxbYJ0UikZ/v37//b/BvTbNsIxU9nng8DqjxRbWhHI3mAFU7RDA03QSSQEQwPzVnzpwvTzCqZBn27dsHuVxuMoYqzRhzoe7dHtRm01U7jREedNnnUQ9uGUPpSmi2qPOgD21Bamtru3l0dPRlpO2XCGS73sXeHFUw0MZZ0RMliAA6FbxPoKL3V2B2syDJSlwD3uvnqUWpSoBSJzSOeOp9CNzsfOGjfxPYnZ2dj+Bny/GZATLG6P10Og1N1k4T36hbRemZP5KEc/YfgNFjR0YKy1UYsV6dMlmqvR5F91rPJD0yab2mF7ZfqxzNz9TIZuDZbPafUGtXOlExtRs1eX5HRwe5XDL9TUDTWF2MVvNCtZn86BOHh6EFLT69uG2vEY/Pwj7323usoaIb88wCcxd2ML2eXCuVVhSlHzX0VrSmf+MEHoGLLtdFaIl/0zDpcXxGwTByoeN0wSJBWzzfhab93GSy6nt7tcL3PP0H5gFrWDlYYD5kvV5ZK6MLQWwj4jxy5Minqf5umo1W+A9QKD5IAB8+fBgGBgYKvOmwCKDpp2nW6oN73oEY7Yis0o2wWpEb2xoxURvR6CLQDg4PD1/hpqX4HEfXah0CPosAJr8any+Iwosy3YiyO8fGDK3WA5B91WoCi8BD+n4eqfk7buDh+y1dXV0PRqPREH3PDrD47kcT2Bft2g3ZCu78m04gjwebrGgCbXBw8MdooK1zG6/Rv/7g7Nmzr21ubmZ2EKUiQNNm9hV79xnjdqUaLabZwgYrQGJoNoKpItifx/cOuY3XKBTfQaA/OjY2NsFV9C1gYm8a//CuXTCqKPBmd5exzLROUj/mvSUGgk4Ec+aopikWixlGFjJa/9DQ0OXoVv1ey5satpUAx/XHcJxeip/vQdoXJR/M7qXVBHbetGU9pJXY2EehhFOdUCNoinNlPRhmZGDReJ1KpV5E7V3Z3d39s1zedlprbI6gcfYQWut/1tLSoiIjcK/7y0qjGqjLA9kPYuOpR7Il5IF6oW+ywO3AyN69e9ckk8lHnNwoa9ryHDTMvo1CEcW2D/s2Rtd5ihYyXrnFLeqhARTPRuDGx7M11NgvoqbvcBuvke6vQ82+CZ9RpwvQ+jgqblifOhQKGRY4vSIlA1rgOFwPXeFlVOOzV2Gb504XoPPH3YZ1syKRCNjxfWoH0vfrCPjni521mhZANyrYdqRMUZTxgRIKptyFYN9RCtjT4vbBRgSbaJsMM8qk3ZYVriOFfwMF4a1iwa410PWEQF0JPVE4GWXjhZT+jWAn+/v7P255CgW3razG6ePO2CyxAyUvLXTJkp8BoAK/y4upu8vzvJjnbaCd+iWdTm8fGRn5XBFaHSq5Y2j2qonCb+k0ZK0TiVwA24ouwwwHt9s4YjOfVomiKGDQ29t73BQcjV0dHR0bFy5cOENVVeHQWQnXw2AnCaliPa7B/EOX8MCQwzDwKJbZCccvo2L4/rDD82+AuaQq/3kKcowUU2cqE8frh5DSf419xCZpG1UgUzLQFOe+cN8+6Glvg8GwAiGPU5CLDUZQxSkSlL+qhIC2fMVKBDfGwHsJb34i6jxSjI1V5POF2B1DVRmXckgd5x3sQ5GsjAHllINUAwOE8D1laAhiwab4qQ00GWN0hsnF7+yFJO2ZDvqzblPZOzWIvueMjsLJaEDticeNGa0gVdENIwwKuEOkbKDpRPtutL6XJBLwdktLALTPIGbz/Gj6m2MO0S0E+Epbmi/Ztt1YDOK1OteX+6NHkLZXoFHW094OB9H3UwKwfUkagrk4nTFOTDb7mtEEh3Gfh5TJwBmoXNTvzDrczyv+5OsmuzOOHIGBOXMChHxQHjrG+f3798OHDxyYqKn2vw/3G/udaHPFsUNqfDqsZrKx+r2HDtfjqpOGo+tRC+SLdu+GMRx/09i3RzNFxijjv1W/d2oUI4kf273H2LYTWOClgZymYXDffnOlLZd8UxpfgdYtw+yEZNLQ8CAV33/n7t0LF+7ZY9ytofsYIPJ1jNYsoE/HsfrJBQugOTDKik4rkLJV62rEItM8/NJFcPxGQ9pyudn3Ew9SKInL+4/AZrTAd8djENGDEbuQIY/67dKdO42TAUsE+nzMd7p89rTv/Eq+HZ0wuGR4ODDMiohFzEAmnIdDXhlkPerx2UBFBlKa2TrvwEGI5rSSN+P5LSB1ujzZSORKnYrD3bxExWwbvSKlMsu/uxgNC7XIilNDhUVdfpxnQeUZ1xPSHmPM9t91o81YH9q0eGbfIfRWKnd2UMVKJu0h67s1m4VUU5NB52ISwSAmmJVMwYqRETgPx6qn8b2dCApVMlKsoJGviZoyK5WChUPDcIFxN6UMGxYthENYn8PRqHGtcK0hpzupT8D2tmUyFb2fsmIlk2tAB7d/dts2eGDZMkiEw0cv/cxPSUWBmSgUZyN9nY0+JAEgEPQPqxo8JnH4I5b1Mp29VeBv00xaNwL8vv0H4OwDB4xzrAlk+v0r39oMu1pboadzhnHkJXV0IcdeEgtkrNtqCj301rgy2LJZ8u/CtKNfZ6EmX/z2rsa9hJRSBjtxxlgaPrN5C4J9igGolNdgGsMvRWGYmxiB+YmEsdk+M24J0WdRsz+Ar9vxuZ+DeTi5cR+0C/2SQ/ex3h0wF7XkBLs8qxOFJQSzUajomI5lRwZge3sb/G7OHOM6A7fzronuF+Dz59J0rBKCpxctMn/fow4hBPeTW7YaIL+w4AQ4EI9PuAaB+mY5gvwXO3ZUJeZQUaCZZZi1p9PwuTd7XA8MbcXPqeMSKAj5XUdrhuhQkg8gEHTIiEBw5KE/eBpWrRnz3Bmn8uxhgjIJwixrfJzsWmECLG7NEM1DoZzMyKSJBqoHaXL36OhxYNL3acEG1UOvgs1Q8ZMDmaURUY9rAHKWBjO38dbKBnWTyzYJ1dqdOln32VTcnJ185aywqRhTrMCLQu2zXhQENOziimpViiBW/ohIa4WoH1J7FF4fNUBYHV6s/VFsbEF49U8VNJpXBeggefdPFULFlQU6iHXXjULwAOTpodk8ALnOUoX6jgcgTw8a5wHI04PGeb1WLEj+ajavtwoF6fgYhB+p/IBJoMlV0epCNhiKUoFm4BxGtCfxBYIcrPasDthHF064Ay5YKUAbO+tVVR5VtQlla3SIGb7R5jBhEKRKgk1TpQLGcqpjv8uch5tCIdBdqJ6Adr1iQBViSULNvcK5dfeELiAiSaAES3lrgTMqGIeoJEClBZdiIlmj8i3xOvmA67q+w3HWiDSX88uFqjF9LANaNgdc4hCXZWhGyQnMrxoijoBlVRVyOSurmowMfKXHOC5x1NoXnUZiMrGiodAV3VLoxJimQxx/oD0cMbRZD6zsmibCk84gE4oMuowMK0kfCknSu9xwQWXezxO69oIbEaNWRyKxyE3p5lgoGo/RVTWBJtdJouGUpj91zpoVJXSrxyE9o1khnuNNjG9Thb7bURLwy7IkXYGG109RiEIByPXF4CFg8SZJfohxvtjjuY1ciN9ypmlpVO2b3PidwG6V5X8MS9L6MBYoIDC268Dbor3w741K0qYo5x8FrzO6EVvQtSH27IkLCbq2qBLZgl+f6RWJIRsAgf9ffOZhHCi2M13PBd1eLQ1G3wqNboTndOz7KxHp8zypndxjTXstm8u9DxFV5aTQyW0a0tTcV5vD4Ye9THT8REHNvwwoG7acFCBQLQNsnCFWSEK/Wh8eHf0c3c8h03EYWjoNeiYDiZHEo9lM5mfBWV6NLxDGTTup5Fcy2WxPGt2wFF1vqNMSVloFmc0KbWCAbhS/O+iuxva9NE1bNTiSvJ28JG7dfyJPjG9qOaHrX0D67kdL7hte18gGqf40mbBSVfWriN8ac48Dm2iYTfDEGUO3S3wTuf0yHWBfQOQNQNVgzEG8hhp8dk5VbxEO93m7xUp0tK7X4xeXIuBfwS/2BYDXJ8iI0+uI0yeymrYC//0HNxtrsmnKlKrrt+LrPSgi70Gp+Ahq/ElYWIfhrwepFilDQysqXw/+++mcrveg75NmR6+ec07/L8AA1yQ2351WYN0AAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHoAAACoCAYAAAAvr/rAAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAG7tJREFUeNrsXQmUXFWZ/u99r15VdVWv6e7shCwQoixBRAyDIjCiDMIojqODx8OMo6OeMagzOuNRORJAnUEcZ0ZI4IAOyL4GSAyMHDiDJCogoECTrROSkK2TTi/VVd21vPfu/P9bku7Ke69rebV1vwv3VLrq1a177/f/3/3//25s46ZNUIU0kzH2OL42Yc5BkCiFMe8FIS6vRp/IVWqUgnk55kiA74TUjZlV44d4lRokMGcDXI9LmWr9EA/6enqkAOgA6CBNpSTXST1o/NanYP/qVh8rAdAA29HF+Di+JqYgw6jA2DJ8fQhzx3QH+gh2xmbDMhdiKrJmyso1BboeNEiy8tRLjNnKVHOF4nXYMVMJ5MDqntJg12EbeNBR06PuPOiw6VFnue47zs0SL6RTi7XiCwXKqdw6F0y5YbSkFNdrfOeXIzCTldsA7DN9QqD5YNDffgDUIEOM3BC19CuQMpXctymn0VMzWhYAHaTpSN3u2kwcTBMhs+D49VYhzHdjTpb4q1TmJ61yx8+o0TKoVzD/LgC6eoli4zdgfpfL50+VAfQpmG92+Ww15t+DuTQqoO5q6DrmEY/P1TLKTnt8lmzQ/grG6MAYC1IAdJACoKuZYhVql5eBGm1Y94q1tlg9w0AXtJrHit0K9CxG6tr2eMMCVHcAqpwtLmOYtzgYdFTuvnp3R1k8DkKSgCOGbFwkUE6vvs1wSlO6BlElBE2yDJqmg4hGgX/m0wDZLHZbjbZLuc9eafju31tulnDwsVOexXr/6mtY4NkO5XKsS8bNtWK1DK9ylPdIBFgoBLnH1gLrOwTDuQzoun60XnL6zrsMtRhQc9ARa4I4AqyqKggEXH/xRYBLLgF+wYfMDs/V1f64sQqVqzeEG0V4IEYQDgMcPAi5//wvA2j1T68DT43B0GA/qMjK7Ch1t7QYf0haDlhTkyEZqNLYXGxvby+wO26H3L33gvTd7wI/aQnqSqq6kwNec9LTOSkKivoYiJ07Qb1lNYjeHSBCCDxHkmtrBa7nEFP1qEZzzw5G7TY6eXAQ1GtXgdi+HaCzc1rPAtWctgmPWMyga+1nN4O+8msABw4AxGOmknJetIV5DHCih5ERgBt/DGzZMhB/exVAc3P1qNwPraYOQCPFYCsqj/72of6s2uyGIIsHHgTx6qugv/kG8BkdJvtO0j+8YCki7R44AvDMM8B+9CODNgwBqHdapc4hoSSjcs8e4Fd/DdhddwEMDZkaQLkRhgYSTLSo4f77Af7nToC33jI1u8C686IoAwd7aG0F2LoV2PU3mON1pIp728mGKKa+JJyY2caNwFavBr7yamMYgl+jsH7xHwDWPg6wbRtAV6cr5TmWS8yA/UDGT9WEhNqOthK7514T8GL6ouTAAv4QQ7D5DRbY1OBKayUaH2ztWlOwyNokYNwyAdHWBuw3LwC78UZgN/wA2Esvm0JKn9H3yR25/XZgN/0E2E9+CpBImJ3nVa6tVYcPG2WStVsVQUfNZffdD+ze+8zfL2G4KG2akqSYfnDLNuDXXw/6NdeY2kP06PeYRcDgEMFWrwF4+ilgqI3iik8AnHuuOXw4PT8wAJyeR38SBgcAWlqO1Xt8am83KfzJJ4HhmAennwY62R9pl4MIUKA5GkAGC7zzDsCuXSBWXYuM0GXWxe+2k2Chy8vuRbq+5x6TqkvVlQ2z5hpq3Y/uVRtKdCdKaBYNFoqQMew0BgJUXYBgHELcjKCZ/1uNQo0WS5eCuOZ7JvjODZ6HRksPvrY41AFVDc4Fp6lFIxCAArRmDbB1603LEn18QyMlyVsQSehszS8kkWFG9Z6MnWxhpueorbNmgbgWwZ7ZDTA66tT2GVYUb7ZDae9gXZeA07EfVA71J2oxQ8o+yjbYNp0CWpyBhn5yfkRTVzV8TIK+gX60Owtxr4qhcZRwTlSWTJr+nV+JNJlA3rABabfFbCiVbzXYNdt+Ji+ieQQcCYZXubadYj9HtI2uDSOg+/pMVvNxTKbxmN13n2lM8vKgKh9o26/bsgX4ddebUl6ukULaSkbUbbcBrFtXlHVZ9USatn8/gr3KGLsNT8QH65rd/4BhfBnl+9B2f2avbLBJs8kaT6dLd1sIZDKUbkWQn1xnSnO9hyKp7WiYse+jZvf3Hws0legnAxpeBsj0b5/GfX+nKaliW7eZmk3WeLHSbQdnkK7hV78yjahGiMLZNG6DfRBpvClamsDYdE3jM/cPHv/no8lgIteLNNum8UJBJk1ecyuwJ9fXvyY7JdJkovFVSON9RdC4ZXiZLtS9ZVnX1QPadr2IxlddZ4JNhpEbldmzMORG3GqNyc3xxpzIsLWSDLTvf//YmO3VdvqchJrG5DJdqOoCbTeAjAiKoBGVjYzoBpXR+JufSQgURYdbVjcWXU8WkSOwv3cNhY0F/q07tj0c1tEfF/CLXxwLhnDeQECPd73eeAPY88/LqLW6MaGQnznnSFcSe/iRY4GNqZDCEZPGN/5WRlC5S9t19txzErvzlyaLVVDACw2YoF/O58kMzpI4p0Xz88E8qXfys8E0LLi5eQF0dV0Aui5PXKBhRF6S8Paup7CRSQJ9ygBNzdS1LAr7fGz7hfiGchyFM55Fel8PyeQQCoNUoGKOIDa7dCF6NKG/isD04b/FZAGTyUKgKJbsYkXi38EvnMOghC0KVH+KIw8MuOo9UvengE+xBanMajtN79JEinNS0Fi9AqTiDmUi8CTKiDuCvA4V6N80IV4SHhsXZNeyOF+B2vvfWN+zxgtpSYmMLVmGaZkq3HbO2GVcki5DrNbldP1biPTWgsZoZAD8Dr9GlqRN40EOUn0nxOwyRZJeV0Khv2aTaTTyPGec3YS8//Wg6xrN2Df4VgnJ0oMqgy5k9ltoFaht9hwFGrmeKbK8Ckn768FivMb27kKSfHNbvPlQYnT0YVrfLWyrmz7lYeXClnjTs4h4QXZGWWN2kEq27wrpd2sKOZ3MZt6dVdWd9JeclSUIM9bcGo3ewYS7v8SOaf5G9ACfxL93MfOqgGBJaHUSLQFsQp1chl75JxHMU91AF/gfWuaRqCzfkVPVP0da12Uy1WVF+SvO2ULNhbJJ/VVdf1kV+pe5pv8JrUgtQLdGrjnFNzT9h4Lx8xGX22XOFzjhJgyrWrogpigrhKpt4lGJK4osfVu4gYw5p2l3j2ra+Vl00BFzLejympN4VtW0Z4az2bMRl99yl4gaaXaIS/8qSbyZRyS+CKn7ZCfKJqd8TNOe7Usmv4gW3BgLWLp+3CliWSEO70sk/lLVtT7X9eUczgfGz+E64+c5xVitKFg6lcuuRJ7PILWbO/SCPq4fo4zw4Lw/oWlfcseFtQiJXchDknSu7kDbJCFpTV2fUnNbkewhlctBIpOBHFnoQV/XFOSspkMGMxMMWsMR0FV9A2r1djetlhlfgErKFrs5ZKqmP8GB6+GQAppOkxs6JFQNRrLBXWW1NMgymkC7CcdfMrgQFxn/FDrcJ9zjHzSb5HxygDA3Um9pylshYiwtDfq7JppM1nVKU62LKdgEVzgj9C1xJoHuArbsHmER0CRJuSaXmRURBEyqngis1pDsFiRJe2i09zSlKEDKArCrZ3xJ3gsTmJhESMquRAB29cbnkt0xPyUuSJXT5rL97nqrUJCO9aeffcrrUfqC5H/ilZDEINVfH/JGqWhA13UIdAD2NKDuAOz67Cve6A0IQK4ToAOwazMm56eqrqrnQkDIPtCtgikrSQVHkahOCm1B8qoTnRvCOWS5cYILhLXCFtlkrHootOyWVnxZvyGsOlYzVQ1oGRtKjdvS2QmSrpdUhm50Fphrld12ouLny/qPGACmjb1j7imWy8FgJAKbO2dg/dxFQ0OAOtJpWDg0ZLShh07rMxZhMFdV5Vjesv5+o9072tthOBwGiWb+6CQtTYeTBwYmrV/DAc2tDWBPLVkMPV1dRuOLTTQzk8lmjA1kYUUBt5ka6sje9sOwaHAIzuzrgzFZNrRVjKNLqg/V4Tfz58Oe1hbY2tEBisfMj8oZdI2OwtJD/ZCWJXilu5tWdrjuCaQ6kDC/g2WHENSerk4YjEaMuWNhCcGlvTvgDKxfukpblSr+K5LVgY8vPdno0OZstqTgPAHLjZ2CSLWcg9eUHAnT221t8OrsWXDR27sMkGx6VjH3IKu8jJ/1W6fvtUxyPhp9kgwpsGneXOPA+gjWg43bBeGW/jB7ttFWovpYNjeBITag0FPByw8g2CG54hNDcqXpmjrXBjlK54BXQXrpd2hM7YvF4P5T3w1zRpJw/u7dkEFtfGrRIkOLqLMVa6wVkxhFwhLYqLWduNAUUVVX4SdB2bB4sbGzwmaehgSa6JGOBX8CQd5mgVxVi1aYS20oHYzH4JennWq8R+BS3XiNtx3ZRiANZyRkZ1hgs0YCmhpBnbr2lKWwvYqa7FWfSJUFrRjWIxqn/jnTGrMr0Ve8EhUnqXwcQSZNjtQY5HpPtmH4NIL92syZECn9DHGvU266fdVo2314vEZ03chgc4vGKS0vjcbpctRvwcT7QJglAH+U/awsWRa2JkcDTS6JDQ2wsR+X9x1GsIsIqjDYgV+8qaLULVl0vdayrgO6LoPGMT+x4AR4sb0VmnxkRNkPkMkHXRvQtW/DH4VMH6BbAVBb3j88DCkfImi8XAk0/eSTDOu6KQDZH9cQzPj4Y3Nnw0sd7dCklb+BlZcjeVQh00+eEYzJFQCb+vhRBPvl9jaIIdjlaHVJ1G1HvMbTdQByZWic4uOPzp1DZxjAOQODMOpgoFHf09geJhzcNtqVImk0g7N+8eKjwZAgVdZAC2MmGqfOX3FkwLphddwFo0KHw7Em2DRznuuEkVzKDw9EIrClcwbEA02unuuF/b5+Zjf0treDFFYmTNMSJgl8b29rq+sUcFFAC4u2NyxeFLhQNaBxMsm2tLVANBI9bnKFwI57bGcuCmgCebs1iR4sD6oBjQNNeermlKvTpIzHVGtRVjfNq74yayakQiFj0iJIjSUkBSXykf80cybsxnEgEhhgUxNo4v9USIadba3GhH1A21MUaDIEDsZi8MfubohowTFjDWm1F+NWhShQcuytVszXWQ6dVkFBpIVd/84Y6+XmgrxFaIh8D98bznsujplmbzaX+FuXYb4Uc/6Fl3Q1379gTuW9fybmr2EezLeXrH7Zn/f8HMyrYOI0olOidjyEbXymyPrT/UvXg3GYPuTG1YeuG/q/ciY1qICrqySQD2HutY5XOhHz37k894Qb0PTdSdZ7fQTzl1w+u9YB6NMxX+Xy/B0OQM/C/IVCGhsKhb4QDodPw/q+OX4BonHsI01wOLeFAP5nlyI7y5nUIC3WqwT0+OtfcwU+5wi2R0p4fObUzjGP553qWJAFa9xlgvVsbW19TJbl5pGREaCcSCSM3N/fD5lMxqkthPyoS7HD0+7+A7uDRB27hxbYJ0UikZ/v37//b/BvTbNsIxU9nng8DqjxRbWhHI3mAFU7RDA03QSSQEQwPzVnzpwvTzCqZBn27dsHuVxuMoYqzRhzoe7dHtRm01U7jREedNnnUQ9uGUPpSmi2qPOgD21Bamtru3l0dPRlpO2XCGS73sXeHFUw0MZZ0RMliAA6FbxPoKL3V2B2syDJSlwD3uvnqUWpSoBSJzSOeOp9CNzsfOGjfxPYnZ2dj+Bny/GZATLG6P10Og1N1k4T36hbRemZP5KEc/YfgNFjR0YKy1UYsV6dMlmqvR5F91rPJD0yab2mF7ZfqxzNz9TIZuDZbPafUGtXOlExtRs1eX5HRwe5XDL9TUDTWF2MVvNCtZn86BOHh6EFLT69uG2vEY/Pwj7323usoaIb88wCcxd2ML2eXCuVVhSlHzX0VrSmf+MEHoGLLtdFaIl/0zDpcXxGwTByoeN0wSJBWzzfhab93GSy6nt7tcL3PP0H5gFrWDlYYD5kvV5ZK6MLQWwj4jxy5Minqf5umo1W+A9QKD5IAB8+fBgGBgYKvOmwCKDpp2nW6oN73oEY7Yis0o2wWpEb2xoxURvR6CLQDg4PD1/hpqX4HEfXah0CPosAJr8any+Iwosy3YiyO8fGDK3WA5B91WoCi8BD+n4eqfk7buDh+y1dXV0PRqPREH3PDrD47kcT2Bft2g3ZCu78m04gjwebrGgCbXBw8MdooK1zG6/Rv/7g7Nmzr21ubmZ2EKUiQNNm9hV79xnjdqUaLabZwgYrQGJoNoKpItifx/cOuY3XKBTfQaA/OjY2NsFV9C1gYm8a//CuXTCqKPBmd5exzLROUj/mvSUGgk4Ec+aopikWixlGFjJa/9DQ0OXoVv1ey5satpUAx/XHcJxeip/vQdoXJR/M7qXVBHbetGU9pJXY2EehhFOdUCNoinNlPRhmZGDReJ1KpV5E7V3Z3d39s1zedlprbI6gcfYQWut/1tLSoiIjcK/7y0qjGqjLA9kPYuOpR7Il5IF6oW+ywO3AyN69e9ckk8lHnNwoa9ryHDTMvo1CEcW2D/s2Rtd5ihYyXrnFLeqhARTPRuDGx7M11NgvoqbvcBuvke6vQ82+CZ9RpwvQ+jgqblifOhQKGRY4vSIlA1rgOFwPXeFlVOOzV2Gb504XoPPH3YZ1syKRCNjxfWoH0vfrCPjni521mhZANyrYdqRMUZTxgRIKptyFYN9RCtjT4vbBRgSbaJsMM8qk3ZYVriOFfwMF4a1iwa410PWEQF0JPVE4GWXjhZT+jWAn+/v7P255CgW3razG6ePO2CyxAyUvLXTJkp8BoAK/y4upu8vzvJjnbaCd+iWdTm8fGRn5XBFaHSq5Y2j2qonCb+k0ZK0TiVwA24ouwwwHt9s4YjOfVomiKGDQ29t73BQcjV0dHR0bFy5cOENVVeHQWQnXw2AnCaliPa7B/EOX8MCQwzDwKJbZCccvo2L4/rDD82+AuaQq/3kKcowUU2cqE8frh5DSf419xCZpG1UgUzLQFOe+cN8+6Glvg8GwAiGPU5CLDUZQxSkSlL+qhIC2fMVKBDfGwHsJb34i6jxSjI1V5POF2B1DVRmXckgd5x3sQ5GsjAHllINUAwOE8D1laAhiwab4qQ00GWN0hsnF7+yFJO2ZDvqzblPZOzWIvueMjsLJaEDticeNGa0gVdENIwwKuEOkbKDpRPtutL6XJBLwdktLALTPIGbz/Gj6m2MO0S0E+Epbmi/Ztt1YDOK1OteX+6NHkLZXoFHW094OB9H3UwKwfUkagrk4nTFOTDb7mtEEh3Gfh5TJwBmoXNTvzDrczyv+5OsmuzOOHIGBOXMChHxQHjrG+f3798OHDxyYqKn2vw/3G/udaHPFsUNqfDqsZrKx+r2HDtfjqpOGo+tRC+SLdu+GMRx/09i3RzNFxijjv1W/d2oUI4kf273H2LYTWOClgZymYXDffnOlLZd8UxpfgdYtw+yEZNLQ8CAV33/n7t0LF+7ZY9ytofsYIPJ1jNYsoE/HsfrJBQugOTDKik4rkLJV62rEItM8/NJFcPxGQ9pyudn3Ew9SKInL+4/AZrTAd8djENGDEbuQIY/67dKdO42TAUsE+nzMd7p89rTv/Eq+HZ0wuGR4ODDMiohFzEAmnIdDXhlkPerx2UBFBlKa2TrvwEGI5rSSN+P5LSB1ujzZSORKnYrD3bxExWwbvSKlMsu/uxgNC7XIilNDhUVdfpxnQeUZ1xPSHmPM9t91o81YH9q0eGbfIfRWKnd2UMVKJu0h67s1m4VUU5NB52ISwSAmmJVMwYqRETgPx6qn8b2dCApVMlKsoJGviZoyK5WChUPDcIFxN6UMGxYthENYn8PRqHGtcK0hpzupT8D2tmUyFb2fsmIlk2tAB7d/dts2eGDZMkiEw0cv/cxPSUWBmSgUZyN9nY0+JAEgEPQPqxo8JnH4I5b1Mp29VeBv00xaNwL8vv0H4OwDB4xzrAlk+v0r39oMu1pboadzhnHkJXV0IcdeEgtkrNtqCj301rgy2LJZ8u/CtKNfZ6EmX/z2rsa9hJRSBjtxxlgaPrN5C4J9igGolNdgGsMvRWGYmxiB+YmEsdk+M24J0WdRsz+Ar9vxuZ+DeTi5cR+0C/2SQ/ex3h0wF7XkBLs8qxOFJQSzUajomI5lRwZge3sb/G7OHOM6A7fzronuF+Dz59J0rBKCpxctMn/fow4hBPeTW7YaIL+w4AQ4EI9PuAaB+mY5gvwXO3ZUJeZQUaCZZZi1p9PwuTd7XA8MbcXPqeMSKAj5XUdrhuhQkg8gEHTIiEBw5KE/eBpWrRnz3Bmn8uxhgjIJwixrfJzsWmECLG7NEM1DoZzMyKSJBqoHaXL36OhxYNL3acEG1UOvgs1Q8ZMDmaURUY9rAHKWBjO38dbKBnWTyzYJ1dqdOln32VTcnJ185aywqRhTrMCLQu2zXhQENOziimpViiBW/ohIa4WoH1J7FF4fNUBYHV6s/VFsbEF49U8VNJpXBeggefdPFULFlQU6iHXXjULwAOTpodk8ALnOUoX6jgcgTw8a5wHI04PGeb1WLEj+ajavtwoF6fgYhB+p/IBJoMlV0epCNhiKUoFm4BxGtCfxBYIcrPasDthHF064Ay5YKUAbO+tVVR5VtQlla3SIGb7R5jBhEKRKgk1TpQLGcqpjv8uch5tCIdBdqJ6Adr1iQBViSULNvcK5dfeELiAiSaAES3lrgTMqGIeoJEClBZdiIlmj8i3xOvmA67q+w3HWiDSX88uFqjF9LANaNgdc4hCXZWhGyQnMrxoijoBlVRVyOSurmowMfKXHOC5x1NoXnUZiMrGiodAV3VLoxJimQxx/oD0cMbRZD6zsmibCk84gE4oMuowMK0kfCknSu9xwQWXezxO69oIbEaNWRyKxyE3p5lgoGo/RVTWBJtdJouGUpj91zpoVJXSrxyE9o1khnuNNjG9Thb7bURLwy7IkXYGG109RiEIByPXF4CFg8SZJfohxvtjjuY1ciN9ypmlpVO2b3PidwG6V5X8MS9L6MBYoIDC268Dbor3w741K0qYo5x8FrzO6EVvQtSH27IkLCbq2qBLZgl+f6RWJIRsAgf9ffOZhHCi2M13PBd1eLQ1G3wqNboTndOz7KxHp8zypndxjTXstm8u9DxFV5aTQyW0a0tTcV5vD4Ye9THT8REHNvwwoG7acFCBQLQNsnCFWSEK/Wh8eHf0c3c8h03EYWjoNeiYDiZHEo9lM5mfBWV6NLxDGTTup5Fcy2WxPGt2wFF1vqNMSVloFmc0KbWCAbhS/O+iuxva9NE1bNTiSvJ28JG7dfyJPjG9qOaHrX0D67kdL7hte18gGqf40mbBSVfWriN8ac48Dm2iYTfDEGUO3S3wTuf0yHWBfQOQNQNVgzEG8hhp8dk5VbxEO93m7xUp0tK7X4xeXIuBfwS/2BYDXJ8iI0+uI0yeymrYC//0HNxtrsmnKlKrrt+LrPSgi70Gp+Ahq/ElYWIfhrwepFilDQysqXw/+++mcrveg75NmR6+ec07/L8AA1yQ2351WYN0AAAAASUVORK5CYII=" + }, + "e1a96183-5016-4f24-b55b-e3ae23614cc6": { + "name": "ATKey.Pro CTAP2.0", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "9d3df6ba-282f-11ed-a261-0242ac120002": { + "name": "Arculus FIDO2/U2F Key Card", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAPoCAYAAABNo9TkAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAD6KADAAQAAAABAAAD6AAAAADrEeKkAAAACXBIWXMAAAsTAAALEwEAmpwYAAACzGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4zMDAwPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT4xPC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zMDAwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cl9EK38AAEAASURBVHgB7N1/jGVZQh/2e+6r7pnp39VdPT1dVd0zuwwLw9iE0PxY2yRuSIRDLLBj5MgEQgw4/iGwHAKJI5wfsmXFimUlVmJHSpRETkikSLEi5a9EimNGOJEcdoddkNdr0AJDdjzs7A4sC7sz01317sk5577qqf5dVe/X/fF5UF2v3rv33HM+p7aqvnPOPaeqPAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwIoEwoqu4zIECBAgQIBAvwXy3wz1rAkxfW763Ry1J0CAAAECBAgQIECAAAEC/RPwH/T712dqTIAAAQI9FPALt4edpsoECBAgQGCFAnnUvHn+xo2vmjbNX6pCeCb98fDL77z55l9eYR1cigABAgQIjEJgYxSt1EgCBAgQIEDgpAIloO+H6YfryeSHQghV08RPpcIE9JOKOo8AAQIECDxGQEB/DIyXCRAgQIAAgQ8E6jjZirGp8s3nIdS//cE7nhEgQIAAAQKLEjhY7GVR5SmHAAECBAgQGKBAUzXX8+h5SuepddF/4B9gH2sSAQIECKxfQEBffx+oAQECBAgQ6LxAHcLFNpx3vqoqSIAAAQIEeisgoPe261ScAAECBAisTiDNbr9YxTzB3YMAAQIECBBYloCAvixZ5RIgQIAAgWEIlP3OQ4jXhtEcrSBAgAABAt0VENC72zdqRoAAAQIEuiBQhs3T4PkLXaiMOhAgQIAAgSELCOhD7l1tI0CAAAEC8wvkgJ5uQQ/nywru85enBAIECBAgQOAxAgL6Y2C8TIAAAQIECFR5yfZqd3f3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gAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCLRX4H8D7duTS/D4+v0AAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAPoCAYAAABNo9TkAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAD6KADAAQAAAABAAAD6AAAAADrEeKkAAAACXBIWXMAAAsTAAALEwEAmpwYAAACzGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4zMDAwPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT4xPC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zMDAwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cl9EK38AAEAASURBVHgB7N1/jGVZQh/2e+6r7pnp39VdPT1dVd0zuwwLw9iE0PxY2yRuSIRDLLBj5MgEQgw4/iGwHAKJI5wfsmXFimUlVmJHSpRETkikSLEi5a9EimNGOJEcdoddkNdr0AJDdjzs7A4sC7sz01317sk5577qqf5dVe/X/fF5UF2v3rv33HM+p7aqvnPOPaeqPAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwIoEwoqu4zIECBAgQIBAvwXy3wz1rAkxfW763Ry1J0CAAAECBAgQIECAAAEC/RPwH/T712dqTIAAAQI9FPALt4edpsoECBAgQGCFAnnUvHn+xo2vmjbNX6pCeCb98fDL77z55l9eYR1cigABAgQIjEJgYxSt1EgCBAgQIEDgpAIloO+H6YfryeSHQghV08RPpcIE9JOKOo8AAQIECDxGQEB/DIyXCRAgQIAAgQ8E6jjZirGp8s3nIdS//cE7nhEgQIAAAQKLEjhY7GVR5SmHAAECBAgQGKBAUzXX8+h5SuepddF/4B9gH2sSAQIECKxfQEBffx+oAQECBAgQ6LxAHcLFNpx3vqoqSIAAAQIEeisgoPe261ScAAECBAisTiDNbr9YxTzB3YMAAQIECBBYloCAvixZ5RIgQIAAgWEIlP3OQ4jXhtEcrSBAgAABAt0VENC72zdqRoAAAQIEuiBQhs3T4PkLXaiMOhAgQIAAgSELCOhD7l1tI0CAAAEC8wvkgJ5uQQ/nywru85enBAIECBAgQOAxAgL6Y2C8TIAAAQIECFR5yfZqd3f3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gAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCLRX4H8D7duTS/D4+v0AAAAASUVORK5CYII=" + }, + "fbefdf68-fe86-0106-213e-4d5fa24cbe2e": { + "name": "Excelsecu eSecu FIDO2 NFC Security Key", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3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" + }, + "ab32f0c6-2239-afbb-c470-d2ef4e254db7": { + "name": "TOKEN2 FIDO2 Security Key", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "973446ca-e21c-9a9b-99f5-9b985a67af0f": { + "name": "ACS FIDO Authenticator Card", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAicSURBVGhD1ZjPi5VVGMf9C9ob6DJoIQi1iDBwI5QgEUEltBJ0YSAGEuRCFBMxIklCayFIQiaKBZUolY7QNJM63nGaca6j40w004zBMBO6LE7n89z7PfO85z3vtdq5+HLufX+c8/k+5znPOfeu+Puvv8LjLDPQGh4O7fHx0GoNp89Vta2dnJysaXp6Kmlubj610vz8XFhYWChqcWnRtLS4FB4+fBgePHxg4rMXjL6VDh482DXQBU9GYjvebic1wQu4BA+4Ps/OzjbCmwFn4r8oGRB0J9odJfh2HX4qgiIP7wU80KXoe3CDfwR4HnWJmeppoKN2DX56qpwytADPz3Ui3wse6P8L7lUxkCsHR3nUBc1nqQTu4b2JEtS/kQJQNxDThbQpwQNH6+HVCprvtMxCDk+eLy5VoXuZKM2Ani8aaMp3g45pY20Gj4BVvufR99GWPEhJvVLH90MwshnoHXkBe3gvD57DM1gvaNQLHFXhF22MZCCHRoB6AVmCz9NFstLYNVCCya+VpOcETn9+jEYDOTiL99+Cl9IG5XCKeK/IV/ro9uvHKhpQmQSyGHGX57M//BBmPvss3Nu1K9zbvDncWbeuprsvvJA08eJLYWb37vD7oUNh4cKF8OfMTBG6BO/BpZoBbVC+XGpxotlr18L0/v0GMvrEE2F0xYow+uSTBjr68sthdPv2pF/2vxduffxx5Roaf+65MPb00513o9qrV5v5+6dOmSEPLfCSAQpHxQDRVVuJeEyVX8+eTdC0d/bsCa1PP7UjSH9/v7WqZD4IDDI3TwpOm+iP69rlhz7/PAzv3dsxHwOBoek33wz3v/22YqAET1sx4NOGBxDgt59/Ptx94/Uw8ckxgxw8csQiOfLsM5Y696/0dQaLUfMp4MUYXKfN75HXjAUDhq6++qoF6taqVWEmzqCglbq0BIV3kgGB0wre8joK6NY334SbmzZZx7fXrAl3PvggTAxdt3sMTKea+g5U3YSXDOm73kVADrdaYXjrVhuPlJsfGrLrYhNnMpBHH0BeuvXdd+HWK6/Y1JLnYydOdE+uLXueTj2I5AEVdV3z92hz0ac0EtNzZP16MwIT1xgXkYqVGZAwwIO26CI4ESDfBwYHDJz7yk8GFAitpO8eNr/vxXhN+Q7TzZgJsIwdOJBmABUNLI6NpQU7/u67tkhJFbsXB1GNJ22m33knlUhKo8oifd6PplVaKZ1LsV8Bs0h/jQHSPcbMwelfYmyqmi3yjz6y72RLxQAP8qKVuFgRbp4+HQZj1Mlxrif4KEBZC3ToxTUAS/cICAseU7V7UUoRwVsbKyBsArasiP2wRtivKgZ4ob1liz0w1Ndnuc51H3XgiTCR18A3Nm4Mww6K6qTPrbVrO/din3atWyrTPRaqrsVnVBC8ZCCZiM8PvvWWPZsMAM8mRUftkyct8lwTvDeBAaaftUFEWBd0Zua7cGjkqafS/sC0mzEHa8UgipnGCCJdc+C8tT0omufdigGmltxXJ8vgndOkFqD028xvdvxmUZVSCmDgF7t5T58UA92n5jMu4h7Paq15CZ6qQ6Amvzhl78NZMUB0WOU2qIu4op6LRcmumdIjUzLQPUqjhQjhn2e9EbTfv/qqCC7xHXhaMoR3L126lBmIF4kQD/l0Ud7n8E3gEtOMAfq2WcRA/MwB0K8FiUUseOTBU/SjOBHw/vnz55cNAEwn148es5QwyIbI87xFnoExwTqIxm2ndkCaAaBzAcaR5OdYplkr6ksppGj7VmJjZazKDGCAmnzj7bc7G1UDvETdZ1AqDP9mcFDj2FExEMFk4I+44EgTiTMW1ymF7O56h7wm2kAzA/Tr4ZU+mL98uW/ZAGlipTFODS+XDPCcPk+89lpn0Pj85JUrthGltHCpRYUBvrQvkDIYSH1FEVUf8ampZQOcvRhjfMMGS59KFQKYSsLgbNuPmgF+jHgYL9KiaX3opNl0DwMGnkUeeBY8s/r9uXP2HLNbMQAY2z+dTZ85UwH20Zf4JZaiHjWycqXBE5kJNsK4iHUPaABJEWYlv0cqAsW7HhxZ2sRxMCB4niN1awbQ5LZt1jGbjwcuifVCJACzTrsAWqh8556kUyzP8B0YqQYfU1MnYUubaPzixYsGzpiVGcjByE9epEaT3/l9hGmJIqAKk6vpSKCWdaBfbDk4lYwFC/xP8acs0ASBdji2xRlAXKNe23EhTjELvPJ71YkaX4OOcEAzQ5LgU5XhzwOne/v2pfEwIHDSi7LJbwNmTSYqBjy4N0Jk2Z0t12PH9uOb36sN4BLwtIL2Eaf1acIZiBSZ2LnT9hNLqaNH7ZDIuByjlW4GH1MNeNrGFMpFBG8e/rDz66i78DDDb1aOyB6eZy1t3FFYAjpv0dUvz1kBEDTCWN/XX1vJxADQEvA1A72MKF0YlKm8fuh9GyztolFshKwZ/ZYmJdiwvDhJEmlE1O2E2n2fvkiX/uPHDVrggOaRLxooQatNcouVyKljHQuImuVrBJPIa/9d4tmrO3aEHw8ftlwHmCrDDivAlO/xB4yuSRz5H5lCTfBeWqwypCgRvZLIZSDRwOCgiecVDFpJsF6A63MyAKDaGnhUL3Ba5TjSQkV5rnvZ3/kO1gu4PF2Q4AlEZQYEnkeeKtRU4/NKg/Iqkx8JJP0zV4HublAG3gMeYYC2ZkDggs+hU4Xpiu+oZMAbEbRaD96BX96cesEr8vpcMfAoeEmwAvc1XvKnSK86+HLOG3gB3v6P6gKrxQTXiwbyDUqpoqjLgIdHAKrN1TPfIzSRL1WaErxaFn/NgAf3Km1KOTzfc3CU57uiTivQkpoiTytVDJTAgbPIZwYED2ATuICbBJTaXL3guVczkIMrbZAHz+Hz1gs4tQaqyEcg+/c5SxstTr9I1Q4MDCZor0YDAs9zHlWi33OxlvMeKLUl+eiT5522mjpSMsCHx1MHwz8ceHy7EhRz5QAAAABJRU5ErkJggg==", + "icon_dark": "data:image/png;base64,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" + }, + "1105e4ed-af1d-02ff-ffff-ffffffffffff": { + "name": "Egomet FIDO2 Authenticator for Android", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAIAAAAiOjnJAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAAB3RJTUUH4gMBDSI3f5N94AAAGeFJREFUeF7t3X1wVNXdB/Bzzt2bfcluSEgIEpJNECXQIARCULQ++FanipSqrbaWcbRTHKsz9o++zfSfp53p03/apx1m2mfGgvWlqHWqdirFl6KWCiKQhJAIQhBIskkw72+b3bu7957ze/7YZN2E7N6XvWeza89nnM40nJvs7v3uueeee14wACBBsBvRKyAIVohgCVyIYAlciGAJXIhgCVyIYAlciGAJXIhgCVyIYAlciGAJXIhgCVyIYAlciGAJXIhgCVyIYAlciGAJXIhgCVyIYAlciGAJXIhgCVyIYAlciGAJXIhgCVyIYAlciGAJXIhgCVx8IYIlVgnIPQ69AjmP0tihQ6Bp8saNpKwMYax3gJANeR8sNjoaOXCA9ffHDh50bNxYcMMN0ooVIl4LLu+DpbW3s8FBxBjt6aG9verRo84dO5y33oocef/W8lp+t7EgHI6dOIFUFWGMCEEYs6GhyEsvRd96C1RV72iBo/wOFr14kV64MOvCRwgoSuTVV6OvvQaRSOpDBb7yOViMqS0tMDU1t0WFMUSjkQMHIn/5CwSDKQ4W+MrjYLHBQbWtbf52OsZIVaP//Keybx8bG5ungMBZHgdLjTfbU90AYowAYh98oPzpT2xoaP4yAjf5GiwIhdQTJ5Cm6RVEanNzeO9e2turV1CwU74GS7twgV66ZLC/Sjt1Stmzh166pFdQsE1+BotStakJQiGDwUKEaOfOhZ9+Wjt7Vq+oYI+8DBYbGNDa242mKo4Q2t0d/uMf1ZMn9YoKNsjLYKmnTrHhYXPBQghhzC5fVp59Vv3ooy/ac2tKYXISpqb0ymVP/j33gKkptakJUYqI+W8FIWxwMPzcc+5otODmm5Ek6R2Q2zSNdnVpFy/Szk7a3S2vX+964AErHwsH+RcsraODdnWZrq4SCIGJCeXPf4apKedXv5q/jxTZ0FD07bdjR45AMIg0DQFglwsiEezx6B2aDfn2sWqa2tQE4XBG30uMIRSKvPoqKIpz+3bscukdkGM0TW1tjfz97/TiRQSAMEaShABYfz8bG5NEsCyg/f3amTPWq6sEjCESibzxBkSjrnvvxYWFegfkCtbfH33rrdiRIzA1FX/uPv0PGEMoxAIBafnytL8gS/IsWFprKxsZsSFYaOaxz9tvQyjk/va3cVGR3gELTVVjTU3R/ftpZydC6Mo6G6JR2t0tb9kyz7FZl0/BgslJ6832eWGMNC32wQegKO6dO0lZmd4BC4b190cPHIh9+CGEQmnePg0EIBLJhYt7PgVLO3eOdnfbU10lYIwYU48dQ6rq/s53SEWF3gFZp1dRfQ5j2tcHwaAIlhmapjY3QyRiW3WVDGO1pQXCYfcjj0jV1Xqls8dgRZUAk5P08mWyZIleQe70X2uOoH192unTNldXyTDWzp4NP/201tGhVzQrVDV29Gho9+7owYNG74IxBkVhgYBeuWww8HJzg9raysbGOAYLIYQxvXhR2btXPXVKryhfrL9feeEFZe/e6Qftxt81Y7S7G+XAsOz8CBYbH1ebmhBjegUzRggNBJRnnlGbmxfmsY+FiioZxrSri4VCeuW4y482Fj17lvX2mvjiZoIQNjQU3rvXHQoV3Hyz6VObAbMtqnlgzMbH2cAAKS7WK8pXHgQLYrFYUxOvZvu8MIbxcWXfPlAU5x13ZOGxD0Sj6vHj0TffpF1dCKW99UsPY1AUGgg4amv1ivLF/SPLHOvro2fPZqm6SsAYgsHIK69AJOK86y7sdOodYB3t64vu368eOwaKYj1SCapKAwHEmA2/KgN5ECz15Ek2Pp7tYKHpb3/k9dchHHbdey92u/UOMA2iUfXYsej+/dMjp22JAsYsEIBwGHu9ekU5yvVgsbExtbl5wb5/GKNYLPrWWxAOux98EPt8egeYYHNFlYAxHRhgIyOSCFYa2unTtLfX+ucev7PLpLbDGKlq7P33UTTq+ta3SGmp3gH6uFRUCRhDKER7exe2pzengwWxmNrcjGIxix89gFRZCYqS6XPr+EyyI0dAUdwPP0zKy/UOSIdXRZUsFqNdXeimm/TKcZTTwWKBgHbunMVMAGCn03nvvdjtVp5/ng0MWPw9SdTmZgiH3Y8+KlVV6ZWdB9+KajYaCICi8GgXGpTTwVKbm2FiwmIgAMjy5Y66OlJcjJ1O5fnnaXd3pucSY+3MGWXPHtfOnY5Vq/RKz5KNiioBY3b5MkxOLmCwOL/DDLDhYfXkSevd34TImzbF+wkddXXuXbsctbU29N0TonV0RPbtg/FxvaLTIBqN/fvf4d/9LnboUNZ64yAYXNg5utl4k9ZoZ87Qy5ctngYAUlIiNzQkfuC49lr3rl2OdevSHGSC240KCvQKIYQQ7epS9u5Vnn2W9vSYe+qXCYwhEqEL+jTa0mnjDyIRtanJ+sNUAMfatXMG6UpVVZ7HHpM3bUp1kFGyXLB5s+6cBVCU6Lvvhnbvjh0+nLWK6nMAtLsbYjG9crzkaBuLdndrHR0Wv98A2OWSN2268lEMWbLE/d3v4sLC2OHDiFIrv58xafly3ZqPdnVF9++PNTWhaDTbkYrDmPX2QiiEjdWstluI96wLQG1pgWDQyolHCAEQv9+xevW8/0hKStw7dzrvvBPJspUGHMaO+Cq6qUE4rLz8cuzIEesdJXNYeJ0IsbEx9tlneqV4seNt240ND2sZN9vTTI7AXq/rwQdd99yDXS5zfwUA+3xyQ0P6xGNZJl7v9MSsDAEghLDbbTqgM0+j9crxYvLlZoXW3k77+01/lHEApKxM3rgxfSnscrnuu8/1jW/gwkIT2QJw1Nbq92jLslRTY/H1J2MMO50FX/6y66GHsNtt4nXGaRrt6kKU6pXjIufaWKAo0812aycGwFFXJxmZEyHLzrvuwl6v8vLLMD5u6M/JstzYaGSqAqmsxG43KIrFSgsAIST5/c5t2+QtW1AkEnvnHWp2aQaMaU8PhMP2PuI0KOeCRbu6tDnr1RoHgD0eubHR6KIMklSwdSt2OpWXXmKDgzrZYkzy+x1r16YrM0Navhz7fKAoegXnwxj2eOTrr3du3x6/sQVCiN9PAwGj7ysOYzY8zIaHJREsBKA2NUEwaKj+uBKAVF1trk8cY3nLFuRyRV58kfb0pPu7GMsbNxp8CI19PlJRYfo5Uryiuvpq5/bt8qZNiRs6LMuS36+a/UziT6N7eqQVK/SK2i+3gsUGB9VTp8ydjGSSJDc0WKj55Q0bsNutPPdcyuVGAPCiRck9rulht1uqqtJaW/UKJmEMe70FN97o3LaNXHXVnH+U/P74mh/mPpxYbKHa7ya/BJypp06Z/pYnAJCyModesz0Vx+rVnu9/37FmzfxtZADH6tXE75/nn1KQamoM9s5PV1TXXut5/HH3ww9fmSo0c2298uc6MKadnRAO65WzXw4FC8JhtaXFyHq18wNwrFsnzXdWDJKqqz27dskNDfNky+mUN2821dkoVVYauuVkDBcVOe++u/AHP5AbG5Esz1sKFxWR5cv1f9scGLPPPoOJCb1y9suhYNGLF+nFi5arK1xYaKLZngKpqHA/+qh8443xMVjTP2VMqqx01NWlPXQuUloqlZeniwIAwtixZo3niSfcDz2Ufvoydrslv990sBCKN7P0StkvZ4LFmNrcPM82EwYBSDU1jmuu0Sunj5SVeR55pCC+zVP8RBIiNzSYnVCF3W5SWZkyCozh4mLXjh2ep56S6+uNfB+k6mpkoKdjFowhGl2QZlauNN5Zf3+mzfbGRruWucJFRe6dO3FhYfTtt1E0ShYv1u1xnYckSX7/5+lMAECEONatc+3Y4fjSl4zf/0p+P/Z4wOy8EgAaCEA0ynWi0ZVyJVhqWxsbGjL3kSUAkKVL5fp6vXImYLfbdf/92OOJ/u1v0urVxNJqZpLfj93uWcuGM0bKygruuMN522140aK0R89FSkqkpUs1s+sMYEx7e2Fq6j8xWBAKZbTwFYBcX0+WLtUrZw52Ol3btpGiIlJaaqrZnkCWLcOLFkF8wjsAcjjk+nrn17/uWLXKXDgQQghhj4f4/cj8UvUwPs4uXzbYA2eXnAiWduGC9fVqAbDXKzc0WAxlerJccNttlsed4sJCqaoqvjgAWbrU+ZWvFNxyi5VegzhCJL8fybK515OYG33ddXpF7ZQDwWJsepsJa8kAkFaulFau1CuXAWsvDCFcUCBVVaktLfLGja4dOzJ/kdPXVrO3OJTS7m6kaVlYKyAhe38pFXr5sultJpI5HPKmTbrjOReKo67OU1Iib9liyyskS5eS4mJqdqQaxjQ+NzqL66xa/C7aSGtttbLNRBwAueoqe5vt9nKsXl1w++22pAohRAoLpepq071ZGLPR0SzvrbfAwYJgUG1psT5mCEBevz4XVkZMydoXJhVZJn6/6Utz/Gl0fB2bbDH5Eu2mnT9vfb3a+HjOxkaLh+cnqabGyqA/VaU9PaaPysCCtrEy3GYCwHHNNVJNjV45Q0ZHR891nAMGxcXFpaWlRUVFHpuuX/aSKipwUZHpkV4Y0+5uUBS7Lsq6FjJYtK9P+/hj6/WNwyE3Nto12bejo+NXv/pVJBKRZdnj8VRXV2+o33DTTTddffXVeodmFfb5pOXLWX+/uc8N4yxviLKQwVJbW9noqLkPKCH+YFhvGpZxlFJFURRFQQiNT4z39fUdP378wJsHvvmNb95zzz3O7HZbp4FdLsnvV1ta9ArOlvUNUSxdg+wAk5NqS4u5vr5kGDs2bLB3LwmMcfx/CSaSJCGEuru7//B/f3j1tVeZ5dfJAamqQuaDHt8QRa+UbRYsWNq5cywQsFhdGZuGZQoAUEoZY5DUwiWEKIryyiuvnDX/IGWOWCw2Nj42NDTU09MTDAb1iqcj1dQQIyO9rhDfEEWvlD0W6FKoqhltM2FwGpYZfr///vvuP9dx7uzZs4qi4JnIEkKGhobef//9NWvWEEuvdmBg4PDhw23tbT2BnonJCYfD8cMf/vCG62/QOy4lUlxMli413YrI7oYoCxMs2tub0TYThqdhGVddXf3kk09OTU0dPHjw6T8+HQwGcdLLa2tvCwaDi0yOR0AIHf3o6DPPPNPR0aFpWvwXejweTbU6ShYhNDPoT/vkE72Cc2VzQxQrX8HMqS0t1reZiK+eYGwalikYY5/P97WvfW3rf21NviBijAcGBkZHR9McO69PP/109+7dZ86cAQBJkuIVHgCA+avYLIRI1dWpBjGnlN0NURYgWGx8XD15MpNmu/FpWBY4HI4NGzYUzB4nE41Gh0eGUx0yLwB49913A4GANDM6FABKikvWrl27uHRx+mN1Eb8fezymm1lZ3BBlAS6F9OxZ1tdnsboyOQ3LGp/PJ8uyqqqJqyFjLBqJpj9qjmAw2Hrq8+lfALB27donvv/EypUrM+96lcrLSWkpnZw09zHObIhidpi1BdmusSAWi504YXp+XIL5aVgLZWJiYmBgIB5NAHC5XA888EB9fb3P55MMjHBPDxcWSmkG1KcysyGKXjkbZDtYLBDQPvnEYqoQQgUFyVOEc9lUaEpNuuh4vd7aVfZtQ+JwSDU1RqZgzJLFJWiyHSy1pcX6erXx3nYOzXYuZtcmEpFctt7Gxgf9ma60EhuicJbVYLGxMbW11fRnkRBfr7akRK9cLgJk9V2nQCoqrAzcm9kQRa9cprIaLO30adrXZ7lTlBQXW5mG9QUVfxpt+ls6syGKXrlMWTrHlkA0Or3NhDUA0po11qZhmeVwOLC1i3Vq9v9Cp1OycBMzsyGKXrlMZS9YtKtLs7w7HAB2uQoaG7PTbC8qKpJndz8yxlSz3T+z36imaWG7L0CS329ltmB8QxTOshWs+Hq1ZvtdEgBIZaW0Zo1eOXvE+7ES/xdjHI1G+/r60hxyJarR5B72cDjc2dWZprwFxO83tO7IFeIbouiVykiWgsVGRrQMm+2NjVno1osrLi5etmxZciwYY8dPHJ8ws2xL/0B/ZGYoAcZY0zTF7nNJSkpIRYXpT3VmQxS9chnJUrC0M2foZ59Zb7aXlMgbNuiVs43X612/bn3yTwgh7e3tL/z5haGhISPXREVRPvzww0gkkugg9Xq9/irzTaK0ppegMS8LG6JYOtMmQSSiHj9u/RHVfNtMcIUxvvXWWyuWVSSP71NV9a9//euPfvyj3//+91Op15kNBoPHjh3bvXv3oUOHEk+dEUK3bL3lGjsWw5kF4+m50aZkZUOUbASLdnZq589bbl2l2maCq9ra2u9973tlZWWJCyLGmFLa0dHxr0P/StMMP/PJmV/+zy/f2P9GNDr9bLGwsHD7PdsfeeQRHuObJWtPo/lviMI/WIn1aq0GS6quTrXNBD8Y47vuuuvHP/pxZWXlnDGlhJA0fQeaOt2WSgx03rp165NPPrmEzygosmQJWbzYdLBmNkTRK2cd92Cx4WGtrU2vVGrxha8sdDFnbGho6ETTibGxseQfxkOWphsdAIB9PuIKAI4ePfrmW29qlpfATAt7PNZW+uO9IQr3YGnt7XRgwHqzvbTUkcVme4KiKHv27nn99ddDoVDy4BlCiNvtJjjl2ykrK9vUuGnx4sWJbI2Nje3bt+/jjz9OdUhGHA6pujoHn0bzbbjYs81EBuvVWnb8+PH33nsPJfWYA8DKlSvvvvvu2traNGOUV61a9fP//vnp06d/+7vfdnd3xy+dIyMjHx79sL6+3vb+d5RYgiYcNtfY0DQaCCBKTYfSGEvn2zB64YL26afm3nBCYpuJ7DbbEUKqqn7wwQfJdRUAVC6v/OlPfvrQtx9q2Nggp74RkyTJ4/Fs3rz5zjvvTJ58cf78+RCfNg2pqMAWevhmlqDRK2cRz2DZsl5trX1jmAybnJw813EuuXZhjN14043XmVm7bHXtarfbnbggDg4OJu4T7YW9XouD/oaG2LC58dbGcQwWGxxUM1n4Kt5s93r1ytlvdGx0YmIiOVhOp/Paa69Nc8iViouLEwPnMcaRSIQyq4vqpDX9NNrs5zyzIYpeOYs4Bkttb2eDg6bfcFx8d7gFWvhqcnIy+SYOAGRZLvKZuzPFZO4bx8jSR2HA9NNos5UWzw1ReAULQiH1xIkMt5mwfb1ag8LhML1iyS7T7W6TZzkTUlWVlaXIMaZdXZyaWbyCpV24QC9dslxd2bLNhGX8qhZO8KJFZNky0zUWxqy/n9OGKHyCRen0erVWgyWtWGHLNhO5g2tYscdjZQnJ+NNoPs0sLsGiAwOZrle7ebOVuj2HUUajMS53hXGS3290s7EEnhuicAmW1taW0Xq15eXy+lmjVvKRx+NxJPXATU1Nne84n6Z8hqw/jQ4EgEM/iP3Bgqmp6W0mrImvV1terlcu1xUXFycGR2CMFUV5/W+vX7p0SdO0TNdumA9ZsoQsWWI6WDMbouiVM83+YGkdHbSz03J1xXGbCcPmXbfD7F2hz+erq6tL/B5CSGtr689+9rPf/O9vLl66mP5YCyw/jYaJCdbfr1fKNLvPX2K9WpOnYVoWtpkwoH+gPxqNJifJ4XD4TG5VQgi5/fbby8vLk0cLdge633nnnc8ucxhWEN9szOx9NMYQDvOYW2FzsGh/v3bmjMVUIYRkueD667O2su+8RkZHjhw+Mqcfy+PxlJlflnL9uvWPPfbYkiVLktcKTD+cKxMWm1mJDVFsZfj5LmNscFBnaBghsY8+YiMjFoMFgL1e7PPRzs5Zc8AliSxbZmWekxmxWGx4eLizs/Mf//jHydaTyc+PGWN+v9/CqmsY4213b6usrDz4z4On2k6NjY3FYjGXy5X5oiDzIhUVn282ZhyfDVGMBgtUNfLaa2prq07rJxIx/Y1JwBimpsJ79szKJQD2+Qqfesqu9dxTaWtr+/Vvfj0yMhIOh+csCSlJ0ubGzYWWuj8wxvXr669be934+PjIyMjI6AgA1PJ5sk68XsnvN71EVHxDlOFhaUGChQAgHIbJSZ1gmXpLV6IU5iz8Go+p5XtMwyLRyMjwiBJR5qSKUrpmzZqtW7emOtAISZJKS0tL7V0sDgCmpiB5igrGZPHi1AekgDGEw7GjRx0TE+kWCwEgJSXSihUG76sMBwshhPH0f1xd+ft5/0WEEEIY4TmPjQGAMVZVVbVr167lWZwjZBDEYsqLL2ptbZ+faYxBUQye+Fkojb7zTuy999JdbRhzNDR4Hn/cYJvETLC+0AABYyxxB1dQUFBSUtKwseG+++5bm5sLJwFAMMhGRuYmydr3UNMg/fw8xlAsZrydI4I1zef1bajfIEmSy+1aWr60uqZ6de1qv99fYPY5STbZew1J/3tM/iERrGl1dXW/+MUvJEmSJCmnw5QnRLCmybKcZiS7YJb5hp4gGCCCJXAhgiVwIYIlcCGCJXAhgiVwIYIlcCGCJXAhgiVwIYIlcCGCJXBh5lkhY1nYNWougAX4o/kCYPq/LDD5hwwHS5Kk+Do+FsaRZQIAu93Y7dYr958HY+zx4KKiLJ0RxrDHY3zkDDY+eRJUNQtDhOeBMZblLH18ecTI9BZb4cJCUl5u8ESYCJYgGGcofYJglgiWwIUIlsCFCJbAhQiWwIUIlsCFCJbAhQiWwIUIlsCFCJbAhQiWwIUIlsCFCJbAhQiWwIUIlsCFCJbAhQiWwIUIlsCFCJbAhQiWwIUIlsCFCJbAhQiWwIUIlsCFCJbAhQiWwIUIlsCFCJbAhQiWwIUIlsCFCJbAhQiWwIUIlsCFCJbAhQiWwIUIlsCFCJbAhQiWwIUIlsCFCJbAhSPy2mt6ZQTBNEfklVf0ygiCaQ4kSXplBME00cYSuBDBErgQwRK4EMESuBDBErgQwRK4EMESuBDBErgQwRK4EMESuPh/5SShTn2Wxl8AAAAldEVYdGRhdGU6Y3JlYXRlADIwMTgtMDMtMDFUMTM6MzQ6NTUrMDA6MDBkEAT3AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE4LTAzLTAxVDEzOjM0OjU1KzAwOjAwFU28SwAAAABJRU5ErkJggg==" + }, + "a4e9fc6d-4cbe-4758-b8ba-37598bb5bbaa": { + "name": "Security Key NFC by Yubico", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "0acf3011-bc60-f375-fb53-6f05f43154e0": { + "name": "Nymi FIDO2 Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAA3NCSVQICAjb4U/gAAAACXBIWXMAAALFAAACxQGJ1n/vAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAjRQTFRFKb7GKr7GK7/GLL/HLb/HLsDHL8DIMMDIMcDIMcHIMsHINMHJNcLJNsLJNsLKN8LKOMPKOcPKOsPKO8PLO8TLPMTLPsTMP8XMQMXMQcXMQsbNQ8bNRMbNRcbNRsfOR8fOSMfOScjOS8jPTMnPTcnQT8nQUMrQUMrRUcrRUsrRU8vRVMvRVcvSVczSWc3TWs3TW83TXM7UXc7UXs7UX87UYM/VYc/VYs/VZNDWZdDWZtHWZ9HXaNHXadHXatLXa9LYbNLYbdPYcNTZcdTZctTZddXad9bbetbbe9fcfNfcfdfcf9jdgNndgdndgtnehdrfhtrfh9vfiNvfitzgi9zgjNzgjdzhjt3hj93hkd7ikt7ik97ilN7ilN/jld/jl9/jmODkmeDkmuDkm+HknOHlneHloOLmoeLmouPmo+PmpeTnpuTnqeXoq+bprObprebprubpr+fqsOfqsefqsujqs+jrtenrtunst+nsuOnsuersuurtvOvtvevtwOzuwezuxe3wxu7wyO7wye/xyu/xy+/xzO/xzfDyz/Dy0PHy0fHz0vHz0/Hz0/Lz1PL01fL01vP01/P02PP02PP12fT12vT12/T13PT23fX23/X34Pb34fb34vb34/f45Pf45vf45/j56Pj56fj56vn56/n67Pn67fn67fr67vr77/r78Pr78fv78vv78vv88/v89Pz89fz89vz99/z99/39+P39+f39+v3++/7+/P7+/f7//v//////Wpo4rAAABClJREFUGBmlwY1/lAMAwPHfdlua2mWkFnVHShEqxIhiUipvkTo0RGJUWF4yUd6Z92rztqJSmBq2pmf3++c8z+1Wd8/urtun7xfPE1Zw6mB3V1f3wVNWgKUN7M20zKwlp3ZmS2bvgKVhCUOdy+qJmbCsc8gScIy+tiZG1ExNXbsgNbWGEU1tfzkGxgw+MYlIas3r3w6YM/Dt62tSRCZtGjQGi703i9C0R7uNOfDoNEKpPRbDQkMPEZr14ilLON1xJaGVAxbCAgfnA5NfDCwj2DoJuOaQBfCsA9OApUes4PBtwPQDnoVndCUhsSVrRdlnE5D83DNw1PcXQcMez+n9SdC431GYd7gZkp9Zhc+SMOOIeTgiWAQTP7Eqn18IiwNH4IiNUPuuVdpdCxlHYM5XCchYtQ1Q22UORoIFsCiwasFCuG7YCEa2Qd33jkNPHWw3gqHTM2GD47IeZgWGMPQaTD7huJxMQochDF0LGYsdvXX2q1aSgQWGUHug7pjF7gM6rOBYHfSoqI/BncbMBRqPWsGdsFFFnQO7jEkTWmEFb8FcFT1eQ+KEMWki71neiQQ1xxTdBdcbl4a5kBq0vOvhbUUfh3XGpWFvI2Qsbx08rmgrbDMuDd3tUN/jqKGjvXknzdkG9yg6Hz4yLg3dwXWwKGtO7/J6RtW/a+RDmK/oDPjJuDR0+3UCthv5YQoF1hj5EWYomoTfjEtBjz4EFx03dDvQNCXv6n1GjkJS0Tr425jBBjii/c2wUv0nQc1eY/6BhKIN0Gdk+J1teS/dCs1ZtRNqPtCfYZpxfTBR0anwi5HNFHrByB1w5ZA9kDLuEFyqaBr2GXmEs2oezho51ACb7IGUcd9BWtEl0GnkxMa1efc/td+852DCjz2QMq4TblH0AdhsWcE8uKkbUsY9Aw8q2g6tltdVCxsgZVwrtCv6BTQNW94aqIOUMdlL4EtFg0bYZ3l9UwmljPkOkoGiLoeMFewklDYmA3epqG/AZcOWl10K3GSx7Ex4S0UdmAx7rKBvNrxhsT0weVDF0FpYZCX/vvmpMQthrSEM9SbgA8flfUj0GsLIvTDntOMQXA0rjWCk9wJ43nHYAhMPGsGcNpjwjVXbPxGeNgdzTs2GK/qt0sk0XDVkDo7oboAlQ1blvxa4YJ8jMG8HsCKwCsEK4FXzcNQGYPmg5zR0D5BxFI7KrgJu/sNz+P1GYFXWUXhGcD/Q/IkVfdwMrAo8Aws8ASQe+duy+tclgCctgIU6G4HmV05b0n87pgPJdyyERXpvIHR5e59j/Nl+GaGFvRbBYsPbmwjV393xqwV+fe2uekIXv5K1GMb1PTmFnNSy9S/v3L1758vrl6XImbLpL+NwrP6t8yhh3tZ+x8KS9rctrqdA/Y1tBywJyxno6sisbm1paV2d6egasBw8T3ie/gevj4H2FDP02AAAAABJRU5ErkJggg==", + "icon_dark": "data:image/png;base64,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" + }, + "d91c5288-0ef0-49b7-b8ae-21ca0aa6b3f3": { + "name": "KEY-ID FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "4c50ff10-1057-4fc6-b8ed-43a529530c3c": { + "name": "ImproveID Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAACXBIWXMAAC4jAAAuIwF4pT92AAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAthJREFUeNrslt9Lk1EYx7/vNte0vXOk7yS7qyWBYvnjIktGU0vDCwktV4KXpv3wB/4BBiIa/QC1wjkVUxNsUuuuzd1k6iBLCxIFzcDXOTZwY8r2sr1rp4uXZuoggryJfS8eeL6c53w45+E5HIoQgoOUCAesGCAGiAEAyX6LZdn19XWGYdRq9T8gkN1qa20VDlVZcZUQYpuZKS0tHTca9ywz6Hurq6s/zs6SP2kXwGI2AzjKqHQ63ft3k4SQpoYGAMWFRXvKLmoLAAwODPwdoLdHD2BkaOh3843J5HK59pTV1dwE8Gp8fP+OS4tL5rfmH6GQkO70oLuzc2jwuSop2dBrOCynk5KO9PX3Z2ZkMCkpqyvfGIYBcL+9w2qdKCoqCgQCAHieF2ofP3xkMr1W0IraulptQYHP7wNF7e2BNl8DIO34CQANd+u7u7oASEABqKupJYRU6a4DoGXxqaoUpZwWA9aJCUJI4QUtgFPqkwnSQwD69ProVxQMBtvb2iiKetDRwfN8KBTiOO7Zk6cA+noNLMsCyMo8zfn9HMflnMkCsLS4OD01DUB39RohxOl0yhMS4iiR3W6PbLszB3FxcbRCQQhRJCZKJBKxWCyTyeRyGoBUKv0y/xmATlcpi4+XyWQajQaAz+ebmpwEUF5RDkClUhVqC3gSnp+biz4HnN8PwO/3R5xAgMvNzk5mkkWUCMDq6nfBdzg2BDCtUABwOl2/fIdAig4IBoORKIjneQVNb3m3ii+XiEHp+wzpGelut/ul0QggEAiUXSm7def2vZaWtLS0hYWvH+Y+5Z/Ny8nNjf5USCSSSIw44XDY4dhQKpXDw8NiiqpvbBwdeVF1owoAu7aWmnrM0KPf3t6+VFLc1Nx8Pu/c6NiYSCSKPsket2d5ednj8UQcr9drX7e73ZtCyrJrVqs1HA4TQpZXVrxer+C7N90Wi8Vms+0fCyr2q4gBYoD/APBzAI6VNqGQPUqnAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,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" + }, + "ee041bce-25e5-4cdb-8f86-897fd6418464": { + "name": "Feitian ePass FIDO2-NFC Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC" + }, + "efb96b10-a9ee-4b6c-a4a9-d32125ccd4a4": { + "name": "Safenet eToken FIDO", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "4b3f8944-d4f2-4d21-bb19-764a986ec160": { + "name": "KeyXentic FIDO2 Secp256R1 FIDO2 CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "5343502d-5343-5343-6172-644649444f32": { + "name": "ESS Smart Card Inc. Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "09591fc6-9811-48f7-8f57-b9f23df6413f": { + "name": "Pone Biometrics OFFPAD Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAaMAAAGjCAYAAACBlXr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAHTmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4wLWMwMDAgNzkuMTcxYzI3ZmFiLCAyMDIyLzA4LzE2LTIyOjM1OjQxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo3YWY3MjAyNS0yZDJhLTZjNGEtOWYyZC0xMjFiMjFjODUwODciIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo2MjZhNDA1ZS1iYTlkLTg1NDAtYTcxYi1kNGVjOWM3MTUxNDIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ZjI0NDI5MDctZDViZS00MWVkLWI1YmEtZjllOWM3YzkyYjUzIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChXaW5kb3dzKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjItMTAtMDZUMTM6MTg6NTgrMDI6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIyLTEyLTE0VDExOjMxOjIxKzAxOjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIyLTEyLTE0VDExOjMxOjIxKzAxOjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjY2ZDhlZmNhLTMzNzItNjY0My1iMjhhLTU3Y2QzOGJkNzBhMiIgc3RSZWY6ZG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjkzMmZjNmE4LWYwMjctMTFlNC1iOTc0LWQ5MmNiZGU5ZmNlNiIvPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyYmYwNzYzNC01MTk3LTRlYjYtYmY3Yy1mOGZmOTZkYWJkMmQiIHN0RXZ0OndoZW49IjIwMjItMTEtMDNUMTE6NTc6MzMrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4wIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpmMjQ0MjkwNy1kNWJlLTQxZWQtYjViYS1mOWU5YzdjOTJiNTMiIHN0RXZ0OndoZW49IjIwMjItMTItMTRUMTE6MzE6MjErMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4wIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8cGhvdG9zaG9wOkRvY3VtZW50QW5jZXN0b3JzPiA8cmRmOkJhZz4gPHJkZjpsaT54bXAuZGlkOjc5MDY4MzA0NzNCODEx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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAaMAAAGjCAYAAACBlXr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAHTmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4wLWMwMDAgNzkuMTcxYzI3ZmFiLCAyMDIyLzA4LzE2LTIyOjM1OjQxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo3YWY3MjAyNS0yZDJhLTZjNGEtOWYyZC0xMjFiMjFjODUwODciIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo2MjZhNDA1ZS1iYTlkLTg1NDAtYTcxYi1kNGVjOWM3MTUxNDIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ZjI0NDI5MDctZDViZS00MWVkLWI1YmEtZjllOWM3YzkyYjUzIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChXaW5kb3dzKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjItMTAtMDZUMTM6MTg6NTgrMDI6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIyLTEyLTE0VDExOjMxOjIxKzAxOjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIyLTEyLTE0VDExOjMxOjIxKzAxOjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjY2ZDhlZmNhLTMzNzItNjY0My1iMjhhLTU3Y2QzOGJkNzBhMiIgc3RSZWY6ZG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjkzMmZjNmE4LWYwMjctMTFlNC1iOTc0LWQ5MmNiZGU5ZmNlNiIvPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyYmYwNzYzNC01MTk3LTRlYjYtYmY3Yy1mOGZmOTZkYWJkMmQiIHN0RXZ0OndoZW49IjIwMjItMTEtMDNUMTE6NTc6MzMrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4wIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpmMjQ0MjkwNy1kNWJlLTQxZWQtYjViYS1mOWU5YzdjOTJiNTMiIHN0RXZ0OndoZW49IjIwMjItMTItMTRUMTE6MzE6MjErMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4wIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8cGhvdG9zaG9wOkRvY3VtZW50QW5jZXN0b3JzPiA8cmRmOkJhZz4gPHJkZjpsaT54bXAuZGlkOjc5MDY4MzA0NzNCODEx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" + }, + "7e3f3d30-3557-4442-bdae-139312178b39": { + "name": "RSA DS100", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "73bb0cd4-e502-49b8-9c6f-b59445bf720b": { + "name": "YubiKey 5 FIPS Series", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "149a2021-8ef6-4133-96b8-81f8d5b7f1f5": { + "name": "Security Key by Yubico with NFC", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "175cd298-83d2-4a26-b637-313c07a6434e": { + "name": "Chunghwa Telecom FIDO2 Smart Card Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIQAAACGCAIAAACT7rX7AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAzbSURBVHhe7Z35UxRXHsADYaPxIJpsElOlMVeZszapzVGp2tqtZJM1ialcu8mAiIhGWEEleMSNho05vCUaSTyiEdw5YBiYAdQBYbgPGRhOM8zADIjIDcPlX7DfLFuW9bXp6e55b/qJXfX5QS37XZ95V/c77nCPXVNgBEUGQygyGIJdGa7Ra47h8Ya+kaorQ3mO3pTK9q9NzZEnqt7YZXl6c86CKENwRNrMMF1QqCYo5H+EamaE6eZGpD0YZXhqU/ZfvyuA/7zT1HS6wp3b0lPVOQRBQYAQLIqIHZiT0TY6UdvtyWzoOphrj0m2vrOv8Kn47OBVaYEq9R2fSAEeBENPxme/vbdwXbL1gNmeUX+l5upw28gEilp2GJLR3D+qqe5Yc/LiyzvMD8caZ69MlSxgKgJU6tnhqYtijC9vN0O9OVPV3tQ/gpIhI/LLgKpQ0TG4SWt7KDoj4KbiowpE98Da9A1nakrdA5AMlDD/I5sM1+hEecfgT4XOjw+X3bMqDRWTn4F27KPvS5IsztJ2Oa3IJsPW7YHMrzttDT9awQjRp6t/KHBAd4KS6jdkkwH9p31ojEFa5evYGerAFRQZDKHIYAiKMmBY4vSMC2aC6jBGZGKmhGoiacmw9Xh2mprWnroohKhfqr/OaoaRLgqEIDrrZRSpNP6d2URvuEVehmv0Wm5Lz7L9RUGhGjSc5+TOEM3S3RZdzeWWoXEUFEG+0NejeKUBqX17b+G5S9003nGRl5HZ0LXks+wAYW8yYLp36IIDBpQoEOKQkgFA1h7fmJVu60RR+A5JGQ7P+C/l7vvWpKPUcwI/sVcTco0NXSgQShCUMcn81frjJa0twyRrMzEZvw6OQScxKzwVJZqTGWHaNSerilz9KBB6EJcB3L1Cl5DR2DwwiuKSDBkZMBYCE/et0d+1XOuV+ZH6/WY7yEOBUOVLQwNKBhHuXaPfYWiAJgFFJw0yMqA3g8K9JAyyVVsgzuFxlAxSQMZJdebkO3AFySgyGEKRwRCKDIaQKKOsfeBUuftkmUsIbga+aBa7+lGqKAHFUuoeQLELRIoMW4/nvQPF90TqYf7Mz7xIffTpavS4LOw0NqG00SJSv2xfkVXS+yvRMmBKsTW1DubPaAZ0MwGfqJ/ZnJPv7EUhyAKNSd9UQOHEa2xO8ZMP0TJ01g6BLzyCI9KSCp3ocbnwpwwAJrZnqtpRGrwiTkbN1eHHNphQxJxAtfgwsdghx/yOEz/LABbHGqs6xX0UECGjdWTi48NlKMqpmB2uc7G0ZM//MoAPEktaPSIKQYQMGEFFHK98Z1+hECRUUqr8XOZCKfQD4ccqRL0MFd1nKNBDkcEQigyGUGQwhCKDIbzIaBuZSMhseCLO9Mj6TK88ut54ssyFQvCd2GQriuiW4/GNxu3p9V6353iRAcPZV3aY0fB5Ku5dnU7jY2rIkXIU0a3IH784X+xtmOtFRpLFOXuloDUGgOpIGY0Fd9NDxqxw3aF8B8oagk8GlOzfD5WiQKciKFR7rLgNhUCE6SEDWLa/iL+l4pNR1j4YLHhP0cJ1mZl0FkFNGxlzVqYWtfG1VHwy9p+3P77RJIysFT9V1PVS2azofxmBKs39aw035ZEA32Q1odzdCJ+MmqvDxe4BIZS4B2queihtsfa/jACV+pUvzabGLpRN3+H/6OSlA2cBuZqpd/cVNfUTWy0oBEUGH+/sLYRKj9JDD0UGH0Ehmo8OlVR3DaEkUUKR4QXozDf+p5ZSd4jgkAERl3cMptZc1gnG0tqHAiGIvDKAoFDNAbMdZdkXytoHOO1yyHAMj284UwMT71nhQtl4pgYFQhDZZQB3LdeiLEsGCjYm2cq5TYtDRkPvyFt7ClFq+IEZCQqEICzIIMvr3xbU9XDMyThkVHUOLfksGz3PQ8Anaq21AwVCkOkn47ENWZWXORaOcMjIbemZGyHiZJVAlfr8pW4UCEGmn4w5K1PP/8pRYhwyTle4YQiBnucBZPC/cvGR6ScDSuxEKcdLVe6a8ZWxUTg7TU2NfRSP0GJNxqKYzIRMXAhiOcfVlnDIYA3WZPx+Tbqh7orrpnT6jiJDNIEhmtCkcoKbXK+jyJDC/Z+ms74pnxJsduAwcUPp9B0OGbXdHrFQfdXMpgzoOUrcA6gcRIGyCXDIeGHbuee25Igi4lglCoQgbMoAnozPRuUgHChklE2AQ4bA03Bu5NnNZ1EgBGFWhi8EhWhQNgEOGXeKmfFNsijGSK+lmpYyYN6HsglwyBA1/Z5kfqTe1HgVhUOK21oG1CD0pFdmhOlgHo7CIcVt3Uw9u+XskvhsUTy1KWe7oQGFQ4o4dS2KTgL3rzWg4iBCgEq9IDoDxSWEZzbnoGwCHDLKOwbLOgZEAY/U9XCM1YgAIaPoJBCbYkXlSAT4gW/W2krbcXRe4TyQkUPGtITeBkvQzNZ5U+xDT8bS3RZSI0lFhq/AsL66i8zhqooMX5mxXFtAaHEMh4xS98DRolZpZNSRf5dJBKqb8j9Pq0fl4BXOb6McMk6Vu2FKguITCMz+Gvv8uj5VIFRliAWKl3MvC4cMs71njuDdSggYd9Ob/fkCUzJmh6cK/exa2Tn0RFwWel44rybk0ptzSIYpGY+sN1UIXKpT3zvyt90W9LxwQPs+s90/i1OFw5SM177J5/y9cshoGR5fl2yFQcKNp+mK4vVv822MVQ6qMgJVGlQCPEDBRp2q5jz+nUMG/KhL3P0ple5kqagvdvj5QGevUJXx8g4zKgEeoGCLXf1CFz5PS6jKeP9gCYpOGooMAoQfJfPVWZFBgDh1LYpOGooMAuzKuYSikwafDFu3p7xj0HdY6Mypyth7/leU5angXKFzHT4ZB3Nbntty1ndWHa/yw0U+/FCV8egGE8ryVPDXIT4ZMAITfooLDzAMj0m2ynhNJ0BVhkBmrdAVOPne7/LJaBudeP9gMQpRGjNX6HafvUT1kjt+WJCxdI9F+kEuwOF8x6xwHQpUGvNX6xPzWlwy+ZBdBvwcD+ba+TcSeJEBU/EXtws9/MsrcyPSdhgavB5IRgPZZTz/+blCb9+gvMiAgtuaWvdglOG+T/VEWLguEzox/9cPSjICVergVakojzfzQFR6vMbmtdf0ImPaQEnGYxtMWU3EllIqMnziT1/lNZDbz6jIkE6ASv3pyYsEm1xFhnSCQjScX08lo8iQzhNxWWRvahYho7xjMPp09T8Ol96K/GHrWVSUvgO9N4oFsfbURa9n2d6ICBlOz/iH35egBCnwsGx/kai7dcQ1U1WdQ4tjjShKBU4W/jNT7B1xovuMlEr3vEg9ilgBERyRdpLrdBB+RMuAxipOXSvkarjblkCVJjalRsLlX6JlANVdw2/tscyNSJ2z0h8EhWpRbuXld6FalMIbgWJ5c1cBtOeo0IQgRQZQ0Np3pMBxON8fvCT4mgL/AGMklMIbgWKRfE+kRBn+5AOWhnAvbT/fPEDrq6UiQwQzw3Rkp9wIRYZQwMQmra2F5p2cigyhvJqQW8a1RZUgxGQ4PRMwmKMBqe/wvhAUos2z9ziGx1DaAMg4KgrJkJHh8IwnZDQuiDLMhbEdaWAoiYrG/wSo1ChVkzy41rAtrY5U20WsZjT1j/4rvX5GGFtzAqrctVy7NdXWQO4KF5J9hn1o7EdLq/CbgW5pYH53KK+F7GJJ8h241np5cawR6jVK/bQBsrYwJpPGtc7kZbhGr5kau974rmBavr8KVGle+yY/o+4KjQV55GVMcvHK0La0+o9/KPUd/7+0D45Ie3tvIUrGJFu0dZWS3jsJgZYMoHVkAoYZvvOef4e2MIpNzGuxD+FkTEJ1xTBFGaTw26QPxtBPb8ox1F1BCfAbioz/MyNMF5JUntfSS+NcbYEoMn4DpqtHChwEl6NJQzYZMBqBebsQ3k+k2GfALHXpHoulte96dDLuW5BNRmPfyM+lrs1aW0yKNSaZjyXxIq65Ecvi9cZVJyp/iyjFuklrO17SRnBGLRb5asbIRGFbX2Ke/a09hbPCCeyP8oW7V+je3FVwwGyHKiLjDiuZ+wzoLcFKgbMv4ljFvavTA24qJqpAdPMi9eFHKy44eiEZMnbdkzDUgdf1eI4WtYYmlb+w7dxD0RkzV+iIv1OBAGeG6RZEZzy/7ZzqSNmPhU4b7/ZTP8OQjEmglajsHNRUd3yX07z656q/fH3h4VgjNCOSxcCD8PiiGOOfd16IPFH1bXazurqj4vKgjM3RVDAn4zqu0Qn70Jit57e96DnNV+FX/Jmm9sPvS17abn44xgjNC/zGg0I1gSoNtDYA/AH+Cv84LzINiv7FL8wfJJbEqWuTLM7s5m4IBIKCAOXaVCgEdmXchigyGEKRwRCKDIZQZDCEIoMhFBkMochgCEUGQygyGEKRwQxj1/4LFNRM4L7whg4AAAAASUVORK5CYII=" + }, + "3b1adb99-0dfe-46fd-90b8-7f7614a4de2a": { + "name": "GoTrust Idem Key FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "998f358b-2dd2-4cbe-a43a-e8107438dfb3": { + "name": "OnlyKey Secp256R1 FIDO2 CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "61250591-b2bc-4456-b719-0b17be90bb30": { + "name": "eWBM eFPA FIDO2 Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAExCAYAAADvDYgqAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAFicSURBVHhe7d0HeBXF2sDxN73QCTVA6FIFFKkCUuyAEumKYkFUbICCIiKCUgQE7L0gdlQsKCpSrIggSC+hJnRCJ4H0b2fveD/0khCSnc2ek//vuXmYd46XkJNz9sy7M/NOQJZFAAAAAABAgQrUfwIAAAAAgAJEgg4AAAAAgAeQoAMAAAAA4AEk6AAAAAAAeAAJOgAAAAAAHkCCDgAAAACAB5CgAwAAAADgASToAAAAAAB4AAk6AAAAAAAeQIIOAAAAAIAHkKADAAAAAOABJOgAAAAAAHgACToAAAAAAB5Agg4AAAAAgAeQoAMAAAAA4AEk6AAAAAAAeEBAlkW3PSszNVXSDyTKqa1b5dSadZK6e4+kHz9m94n3//mAcQEhoRJcupQER0VJWJVKEt6gvoRXryZBpUpJQCD34QAAAABf4NkEPSsjQ05t3iKHPvpEjv+wQNL37ZOs1DT9KICzCYyMlNAa1aTENZ2lZJfOElqhvPWOD9CPAgAAAPAazyXoKjE/Mvc7SXxzhpxasVL3AsiPgNAQKdqxvZS9Y4AUadJY9wIAAADwEk8l6Md/+132jHtKUtat1z0AnFa869VSYdgQCatSRfcAAAAA8AJPJOgZJ07InolT5PAHH4tkZupeAKYEhIdLhVEjJKpXdwkIDta9AAAAAApSgSfop7ZslR0DB0nq1u26B4ArAgKk2BWXSpXJEySoaFHdCQAAAKCgFGiCfuLP5RI/YJBkHDmiewC4LbxRQ6n25isSEhWlewAAAAAUhAJL0E8sXSY7brlDMpOSdA+AghJaq4bU/OhdCS5dWvcAAAAAcFuBHJCslrXH33kvyTngEambt8r2gYMkg/ckAAAAUGBcT9DTjx6T7bfdIRmHDuseAF5w8s+/ZOcjj0kWhRoBAACAAuHqEnd1xnn8Aw/JsS/m6J5zFxgZIUGlSklIjeoSVKK47gUKOettnL5vv6TtiJeMI0clKy1NP3DuKk4YK2X69NIRAAAAALe4mqAfXbjILgp3zkepBQZK+PkNJKp/PynWuqUElykjAUFB+kEAf8tMTZXUXbvk6Lfz5NDM9yV9z179SO4Fliwh5/3wDUXjAAAAAJe5lqBnJCVL3FXXSFrCTt2TO6E1q0vFUSOkeNs2dqIOIHcyT56Ugx98LPufeV4yjx3XvblToltXiZk6ybpCBOgeAAAAAKa5lvEemfP1uSXnVmJQomes1J4zW4pf0o7kHDhHgRERUvbW/lLLeg+po9TOxbFvv5dT8Qk6AgAAAOAGV7Jetex2//Mv6SgXrGQ8atBAiXlqvASGh+tOAHkRVqWyfYRa5MUtdc/ZZZ1Kkf3PvqAjAAAAAG5wJUE/sXiJpO/ao6OzCAiQ0jf3k+ih97O8FnCIutFV7dUXz2km/cSCRZJ+5KiOAAAAAJjmyh50Vbn96Gdf6ChnKoGo+fH7EhgWqnvyyfrxstLTJf3ECck4flyyUvNe3RpwizqtILhYMXuZul0Q0aGbVae275DNV14jWSkpuidnlZ6ZIqWv6aIjAAAAACYZT9BVcryueRvJPHxE9+QgOFiqz3pPijZprDvyLnndejk2f6Ek/bpYUrZslYzEg/oRwHcEx1SRiNq1pGiHdlKsY3sJq1hRP5J3+156VfZPmqqjnBW9rKNUf/VFHQEAAAAwyXiCnrxmrWzp2l1HOSva8RKp/vrLeZ4tzDyVIke+/U4SX3tTUtZt0L2AnwgMlKKd2kuZW/tLsRbN8/w+ST96VDZ1vFIyDh3WPdkLKl5c6v7xswSGhekeAAAAAKYY34Oe/NdK3Tq70n165S3pyMqS44t/l7jO3WTXkOEk5/BPmZlyYt4C2X7DLbJt4CBJyWOV9eASJaTEtV11lDN1VFvqzl06AgAAAGCS8QT95PrcJcsBkRFS7JK2Oso9VSF+98TJsuOmAZK6dZvuBfyYStR/WCibu14nh+d8Y8fnqkSXq3QrZ1lpaZLC+woAAABwhdkEPStL0rZu10HOwhvUl8DQcysMp4q+bb/jHjn46pv2XnegMMk8dlx2Dh4me6Y+Y7/XzkVEzRoSWLSojnKWsieXJzAAAAAAyBejCbra3p6RnKyjnKmzms+FSs633TpQkhb9pHuAQigjQxJfeMVeRXIuSXpARIQEl43SUc7Sd5OgAwAAAG4wO4OemSmZuTzOKahCed06O7XsNn7YCDm5bIXuAQo3tYrkwFszdHR26ui2gNDcFX7L2LdftwAAAACYZHwPugn7X3tTTnz3g44AKPsmTZOkFX/pCAAAAICvMXrMmtoXvqlLrKRujNM92YsaNFCihw3VUfZOboqTLV2us2fRcy0w0N5vG1wmSgKjSulOwKOsd2TGzl2SceKEZJ5I0p25E1qrhtT+8lMJjIjQPWeWlZEhcZ1jJWXjJt2TvZLdukqVaZN1BAAAAMAU30rQrX/qttvukBMLc7nv3D43uoOUHXCzhNerK8HFiukHAI/LzJS0w4flxO9/yIHnX7ISaes9lJu3akCAlB8xTMrdfqvuODMSdAAAAMB7fGqJe9Kq1blOzkNiqkj1j2ZK9VdfkKLNm5Gcw7cEBkpIVJSU6nyV1J4zWyo+OVoCwsP1gzmwkvjEl1+TjKRzm3kHAAAAUPB8J0G3Eo8Dr76hg5yFn99Aas7+SIpe1FT3AL5LFXQrc30f+4ZTUMkSujd7GYcOyxF1PjoAAAAAn+IzCXr6kaOS9MtvOspecMXyUu31lyWkdGndA/iHIo3Ol8rPTbVe5EG6J3tHPvtCtwAAAAD4Cp9J0JNWrpLMY8d1lI3AQKk4drSElCurOwD/UrzNxVLqhj46yl7ynysk4/gJHQEAAADwBb6ToP/2u25lL7xeHSnR4RIdAf6p7IBbJSA4WEfZyMiQpJUrdQAAAADAF/hMgp68bp1uZa9El6vt/bqAPwurXEkiWjXXUfZOrV6rWwAAAAB8gU8k6FkZmZK2abOOslfssk66Bfi3Ym3b6Fb2Uvfv1y0AAAAAvsBHEvR0O0k/m7AKFXQL8G+h1avpVvYyT3DUGgAAAOBLfGaJe64E6D8Bf8drHQAAAPA7/pWgAwAAAADgo0jQAQAAAADwABJ0AAAAAAA8gAQdAAAAAAAPIEEHAAAAAMADSNABAAAAAPAAEnQAAAAAADyABB0AAAAAAA8gQQcAAAAAwANI0AEAAAAA8AASdAAAAAAAPIAEHQAAAAAADyBBBwAAAADAA0jQAQAAAADwABJ0AAAAAAA8gAQdAAAAAAAPIEEHAAAAAMADSNABAAAAAPAAEnQAAAAAADyABB0AAAAAAA8gQQcAAAAAwANI0AEAAAAA8AASdAAAAAAAPIAEHQAAAAAADyBBBwAAAADAA0jQAQAAAADwABJ0AAAAAAA8gAQdAAAAAAAPIEEHAAAAAMADSNABAAAAAPAAEnQAAAAAADyABB0AAAAAAA8gQQcAAAAAwANI0AEAAAAA8AASdAAAAAAAPIAEHQAAAAAADyBBBwAAAADAA0jQAQAAAADwABJ0AAAAAAA8gAQdAAAAAAAPCMiy6LbjstLTZVOXWEndGKd7shc1aKBEDxuqo3/KTE2VDa3aS8ahQ7rnzBqsXS6BkZE6Mic1PkFOrd+gI/iz0JgYCa9XR0fecWT+AkkYMEhHZ1aiR6zETJ6go3/KysiQuM6xkrJxk+7JXsluXaXKtMk6AgAAAGAKCXoeHJz5vux+bKyO4M+i+veT6Mcf1ZF3kKADAAAA/ocl7gAAAAAAeAAJOgAAAAAAHkCCDgAAAACAB5CgAwAAAADgASToAAAAAAB4AAk6AAAAAAAeQIIOAAAAAIAHkKADAAAAAOABJOgAAAAAAHgACToAAAAAAB5Agg4AAAAAgAeQoAMAAAAA4AEk6AAAAAAAeAAJOgAAAAAAHkCCDgAAAACABwRkWXTbcVnp6bKpS6ykbozTPdmLGjRQoocN1dE/ZaamyoZW7SXj0CHdc2YN1i6XwMhIHZlzcvUaOf7jzzryvuQ/V8jxRT/pyFnlB98rEuS/93kiGp0vxdq10ZF3HJm/QBIGDNLRmZXoESsxkyfo6J+yMjIkrnOspGzcpHuyV7JbV6kybbKOAAAAAJhCgl4IJL45Q/Y8ceZELb8axq2RgOBgHcEt/pygZ6WlSVamsctS4RMgEhgSYv1pNQAA/0ONM8WFj52A4CAJCArSUcFz9fOWzyIg10jQCwESdP/jzwn6tqHD5NSKlTpCfgWVKC61Pn5fAkNDdQ8A4HRx3ftI+lnGmE4oP/wBKX3VFToqWBlJSbLlxlsk4/AR3WNWZItmEjP+CQkIZHctcDYk6IUACbr/8ecEPa7fzXJy8RIdIb9Ca1SXuvO+0REA4N/WtWwn6QcO6Mic6EnjpUz3WB0VoMxM2T50mBz7yp3PhuAK5aX27I8lpFw53QMgJ9zGAgA/Flarpm4BACCS+NEs15LzgLAwqTJ1Esk5cA5I0AHAj4WWL69bAIDCLnndetkz7ikdmVduyL1SrEVzHQHIDRJ0APBjYY0a6hYAoDDLOH5c4gc/KFknT+oes4pdcZmUu+0WHQHILRJ0APBj4TVr6BYAoNDKypLdk56W1C1bdYdZIZWipcr4sRSFA/KAdw0A+KugIAmrUEEHAIDC6vA338rhDz7WkVmBkRES88IzElyypO4BcC5I0AHATwWVKiWBxYrqCABQGKXEx8uukaPtWXTjgoKkwqhHpMj5bK8C8ooEHQD8VFDRIhIYFqYjADArMzNTTp48KYcOHZKt27bJ0qVLJTU1VT+KgpB5KkV2DH5QMo8f1z1mqaNZy/TsriMAeUGCDgB+KqRyJQkICtIRAOSNSrzT0tIkOTlZEhMTZfPmzbJ48WJ57/33Zdz48TJ4yBC5NjZWatetK+fVqyd1rK96DRpI67Zt5bhLiSHOQO07f2qynFq5WneYFd6ooVQeO1okIED3AMgLEnQA8FOhVWN0CwByphLwAwcOyNq1a2X27Nny4ksvyWOjR0u/m26Si9u1k6bNmtlJd6WYGKnXsKG069BBbr71Vnl87Fh5wfpvv5k7V+Lj42Xv3r1y5OhRO6lHwTq66Cc5/P5HOjIrsGhRiZk+RQLDw3UPgLwiQQcAPxVKgTgAOTh27JhcevnlUrd+fYksVkyiq1SRJk2bSq++feX+IUNkwlNPyUcffyzLli2T9Rs2yO49e0i8fUTq7j2yc/gIyUpP1z0GBQRI9JOPS3jVqroDQH6QoAOAnwq/oJFuAcD/UvvDF//+u2zZ6s7RW3BHpvV7jX/oEck4dFj3GGQl51EDbpbSXTvrDgD5RYIOAH4qvEoV3QIAFBb7X31dkn/7XUdmRV7UVCoOHawjAE4gQQcAPxQQESEhZcrqCABQGBxfslT2P/eSjswKrlhBqj43VQJDQ3UPACeQoAOAHwouX04CQoJ1BADwd+lHjkjCsIeshvl95wGhIVJ58gQJKcuNYMBpJOgA4IdCSpaUgEAu8QBQGKhicPHDH5H0XXt0j1ll7rpDirdqqSMATmL0BgB+KKRGNc6iBYBC4sDb78iJ+Qt1ZFbR9u2k4r2DdATAaSToADwlpFxZCa1S2bWvkIoV3Ulkre8RUin6jP8GE18R9evrbwwA8GdJK1fJvqnP6MisEOvzJWbKRG4AAwYFZFl023Fquc2mLrGSujFO92QvatBAiR42VEf/pI6L2NCqvWQcOqR7zqzB2uUSGBmpI/wt8c0ZsueJCTpyVsO4NRIQzD5Xtx2Zv0ASBuR897pEj1iJmXzm33tWRobEdY6VlI2bdE/2SnbrKlWmTdaR/zkVHy9xl3U2flZsYJEiUmfBdxJSJkr3AEDBSkxMlKo1atjHrZmyd9cuiYry9nVvXct2kn7ggI7MiZ40Xsp0j9WRM9KPHZO4bj0lbUe87jEnMCJCqn/wjhQ5v6HuAWACM+gAAACAD9r1xHhXknMJDJTyjz5Ecg64gAQdAAAA8DEHP50tRz/7QkdmlejaWcr06qkjACaRoAMAAAA+5GTcZtkzZpyOzAqrc55UGTeGk0EAl/BOg09R9Qi29rtZ1l3QwvhX3LU9JONEkv7OAAAABS/jxAmJv/8ByUwyP0YJLFZMYp6fZu8/B+AOEnT4jqws2Tf9eUn69XfJOHLU6Fdm8kmpOHqkBBUtor85AKCwyMzMlPT09DN+ZWRkWB9HxurrAjnKsl6buydMylWR13wLCpToMaMkokYN3VF4qfd8TtcF9RjgFKq4FwL+UsX92M+/yI5b7xTrSqh7zCl7/91SYfC9OvIeqrg7hyruvi8lJUXWrl0rf61cKfv375cDiYn6EZGw0FApWbKklC1bVmrXri316tb1fEVpuCcpKUm2bt0qq1avlj179si27dtl48aNcurUKTl58uQZB90RERESEhIipUqVkgb160ulSpWkatWqdrty5coS7EMnm1DF/T98qYr7ke++l/h7hqi7SLrHnNL9+0nlUY8UuiPV0tLSJN4aGyxfsUISEhIkLi5ONllf6rqQnJys/6v/p97zYWFhUrx4cWnYoIF9HahWrZo0btTIvj740jUB3kCCXgj4Q4KeunuPbLZeSxmHj+gecyJbt5QaM9/09F4rEnTnkKD7pqNHj8rcb7+VDz78UH786Sc70cqtOuedJ48/9pj06NFD96AwUMn2tm3bZPHvv8uiH3+Uv/76S1auWqUfdUZ4eLg0b9ZMLrjgAmnVsqW0bNHCHqB7FQn6f/hKgp6SsFPirukumceO6R5zIi5sIjVnvi2B4WG6x3+p17+6wfvLL7/I/AUL5PclS+SYQ89xpJWXtG3TRtpfcom0sK4HzS66yL5OADkhQS8EfD1Bz0xJka3X95eTy//SPeYElS0jtb+eLSFly+oebyJBdw4Jeu78biU169av15EzLrIGKo3OP19HuXPAGkS//OqrMuXpp884k5Fbsz76SLpde62Ozt2sTz6R48eP68h5V15xhURHR+vIGZ999pkcOXpUR867xBqA1vTYUli1HH3Tpk3y2ezZ8smnn8r6DRvsPrcEBQXZN4R69ewpV115pTRo0MCeaTNp1apVsuzPP3WUsxMnTshDI0bYS3RNeXryZClatKiO8q58+fLS+eqrdeQsX0jQs9LSZHO/m+XksuW6x5zgcmWl1uxZElqhvO7xP+o1r27QfWh9Frz73nty8OBB41tXAgICpFixYtKje3fpd/310rx5c+PXgzNRNys//+ILOXLE7KRX6dKl8/U5m1fq53tn5swzroByirrJ0rtXL/sabwIJeiHg0wm69fLc88zzkvjsC1Zb9xkSYF0kq779mhRr2Vz3eBcJunNI0HPn/iFD5MWXXtKRM+675x55esoUHeVMDaZmzZolQx98UBKtgVR+qOWGf/7xh9SvX1/3nBv1sdmoSRPZsHGj7nHet998I506dtSRM5o2a2Yv5Tblnbfflr59+uioYKkVFd9//71Msl5fahCulqwWNDWQU0tfB9x6q/08xcTE2AN2p02dNs1Ouv2NSmo+sBIpEzyfoFvXnF0TJsvBN97SHeaoMV3V11+S4m3b6B7/om7sfj9vnjw1aZKs+OsvV2/YnU6996tXqyaD77/fTvRUMuum2wcOlLffeUdHZqibEbsTElxfMaC2LdU//3yjv9sO7dvLd3PnGrmGKxSJg6cd+22xHHzhFePJufUOkzKDBvpEcg74i4SdO3UrZ4cPH5brb7hBbr7ttnwn54qazVOJEvyPWqr63vvv2zcjevXta88keyE5V9RgcceOHTJq9Gj7Bs+1sbGydNmyAksQfE3rVq10q/BRNXgOzjCbTP2tzF23+2Vyrm7yfvnll9KkaVPp2bu3fW0oyPeeutG7dds2uW/wYKnXsKE8PXVqvlaFnatevXrpljlqlZmqD+O2JUuWGP/d9rFeQ6aSc4UEHZ6VdiBRdj3wsPGZTSWyWVMpf9dAHQFwwxrrg/tsi7jUnuG27dvL7C++cGy5WpkyZew7+/Af6nX0888/S5t27eTmW2+VLVu36ke8KfnkSbuGgvr3XnHVVfYWEuSsRiGtJJ66b78kDH/EyjDNJ5NF2l4sFe69W0f+Q23PurpLF+luJaXqM8VrDh06JA8/8oh9Y/GLL7886+eiEy6xrj2q0KVpc7/7TrfcM3/hQt0yQ60IuC42f8Uez4YEHZ6k9lolPPiQpFsfTKapvVYxz0+XgJAQ3QPADceOHrUrZWdHVc7teOmldlVtJ114wQVG73zDXWof9dAHHpDLrrzSXrLqS9RNJ1XkUN2E6t6zp2zc5MLRWT5I7dNVVfILG7XFM+HhRyTjwP+fTGFKcMUKEjN5ogQY2lNbENSKmslPPy0tWrWShYsW6V7v2rxliz273++mm+x6KyaFhoZKd8NJprJ48WLdcoe6pqoioCapmxvqdBiTSNDhPVlZsu+lVyXpp191hzkqKa80aZyElC2jewC45djx4/by9TPZvXu3XG4lXDt37dI9zimMA31/pWbD2nfsKM+/+KLPLxX/8quv5KLmzWXsE0/YNx3w/0KCg6VChQo6Kjz2v/G2O2OhiAip+vx0vxoLqWMTu15zjTwycqR9PJqvULPnH8+aZc+m/7F0qe4147rrrjN+s1otcTd5SsS/qaMy1VYik27s10+3zCFBh+cc/32JJD7/so7MihpwixS/pJ2OALhJzZ6rs2b/TRX46tWnj5HkXFHVxuH7llqDV7VE3Omj0gqSSiSeGDdOWl58saxYsUL3om7duoXuaKrjfyyVA9Of05FBgYFSYfhQKdKkse7wfStXrrSvDQt8YNY8O3v27rVvUs98911jS95VXQd1DJxJu3bvNp4wn27evHm6ZUZERIR9yoppJOjwlLT9+yXh/gftJe6mRTS/SCoMvU9HAAqCutt9OrU8bcgDD8iSP/7QPc4KCw0ttHtZ/Yk6r/iKq6+W/S5U3i4IaltH3379XJ158rKaNWvqVuGQfuy47Bz+iCs1eIpfeZmU6Xe9jnyfWlLdvlMniU9I0D2+S92sHnjnnTJt+nQjSXqRIkWk2zXX6Micb13ah66eo3k//KAjM9TpKiVKlNCROSTo8Az1QaQKobix1yooqrTETJ1k/Ax3ADn77V/709TxN2rGwJSyZctKKcN7x2DW+vXr7RUWJs+h94LJTz1l7xOFSLOLLtIt/6eOQU0YOUrSEnJ3ykV+hNauKVUmjpOAQP9IB3788Ue5qksXv9oioqrPjxg5Up559lnd4yxVjdy0xS4VwTyVkiJ/GLq5/7cBt92mW2aRoMMz9r/+piT9+IuOzLH3nU8eL6GVonUPgIJy+hL3o0eP2kfOqAGJKeXLl7cLTsE3qWrHsT16yIFE8zdyC9KdAwdKVyvRwH80bdpUt/xf4rvvy/FvzM84BhaJlJjpUySoSBHd49vUqqtu3bvbs87+Rq0sG/bQQ/Lee+/pHue0bNlSihcvriMz1LFnKVbybNrmuDjZu2+fjpynzqpv17atjswiQYcnnPhjqeyfPF1HZpUecLOU6NBeRwAKkpoN/dsbb75p/AicphdeSAV3H6WWL6qCT1u2bNE9/kkVMZwwfryOoPaeV6taVUf+LXnNWtk7eaqODAoMkIpjRklk3bq6w7dt375duvfo4ffFFe+8+27Ht3+pauRXX3WVjszYt3+/7Le+TPtm7lzdMkMtb3friFYSdBS49EOHJGHIcHWLUPeYE9mimVQcwr5zwCvUHmK1VDkxMVHGjB2re81RxabgmxYtWiRvzZihI/+k9oS+/eabUrRoUd2DkiVKSFRUlI78V/qxY7Jj8IOSddJwxfGAACnd73qJ6nat7vBtasa8d9++dhLo71QRyRv69bNXEjnJdFVyNXv+7+1sTlOrDEzudVc39u+4/XYdmUeCjgJln3c+bISk796je8wJKlVKqkyfzHnngIeo5ez79u2T995/X5JzOBPdKer8Uvge9ToZNXq0PQjzV2oAOOKhh6RJkya6B0r5ChXsysl+LStLdj05QdK2/bNopgnh9etJ9EMP2om6Pxj75JOy3MUTD4KCguxio2plh/pSW6ZCrHGlWyuzdsTHy52DBjl6LWzerJmUKWP2iL1vv/1Wt8w4euyYrDttRZ7ToitWlGbW8+QWEnQUqP1vzZATC37UkUHWhTN6/BgJLYTnqAJepqpUr9+wQV562fzRimogVa1aNR3Bl6iq7aYq+3uFOvJo6JAhOsLfGjdqpFv+69CXc+ToZ1/oyJygMlFS9aXnJNBPjqz7+eefZeq0aToyRxVrvOLyy+WF556TX3/6SbZt2SJ7d+2yv/bs3Ckb1q6Vb+bMsW+w1a9XT/+/zPnK+l5OzharquQdDB8/+rN1Dc/IyNCR89TJF06vLDhd+/btjR9JdzoSdBSYE38skwNTntGRQVZyXvq2/lLyyst1BwAvefOtt2TL1q06Mqdq1aqufsDCGWrv+bPPP68j86pUqSI333STPD15ssyzBsEb162TrXFxsm/3btm0fr2sW71a5n//vTz3zDMycsQIuaZrV6lXt64E5+NUkKjSpeWdGTPsmTj8U+3atXXLP53avkN2jx5rz6KbFBASLJUnPCFhflIgV+03HzBwoI7MULPivXr2tN/zc778UgbefrtdsFCdBqK2o6gvtSc5JiZGLu3UScaOGSPLly2T2Z9+Kuc3bKj/FuepFUX3Dx7s2J579XPecL3Zo/ZUYc+9e/fqyHnfWddkk9xc3q6QoKNApCUelIT7H3DnvPMLm0jFB5mVALxqztdf65ZZlaKj85VEoWAcPHhQfvr5Zx2ZU716dXlv5kw7IX/t1VflvnvvlfaXXGKfm6+SdlXBV/03KmFs166d3HnHHfL46NHy6axZsuLPP2VXfLx8/OGH9kC3SuXK+m/NnSmTJkmM9T3wv/x5W0pGcrLEW2OhzOPmi5tF3XaLlOjYQUe+b9KUKbLVYFHR4lbiPXPGDHn3nXfsm7u5pZbAd+ncWRb/+qud0JuyfccOef6FF3SUf5dY1zpVMM6UZOu1/qd1nTRB3cT94gtzK1DU7/8il496JEGH67IyM2XX6LGSvtfcUQh/U8u5Yp6dKoEcqwQUem5/wMIZq1evto/gM+ni1q3lj8WL7dmyvMxiq0G5SuBju3Wzi7ytW7NGflq4UHr26HHWI4xu6NtXbrjhBh3ln7pxsNMavOfma9WKFcbPWl/1119n/N65/VL7Y/2RGgvtfmqKnFqzVveYU6RNa6k49H4d+T61D9vJ5PTf1EqrD99/X3r36mXPLueF2lL17PTpcv+99+b57zibqdbf79S1Ua0GuLRjRx2ZMX/BAt1ylqoQb/J0j8svvdT11U0k6HBd4tszXTnjU4KDJHrCExIaXVF3ACjMateqpVvwJaZnz1Vi/cF77zk6e6SKR7Vq1Uref/dde3nsxPHj7RUc/x6oq5n2p59+2tEBvEou1Hn/uflSS3VNK2d9jzN979x+qZsf/ujoDwvk8Acf68ic4HLlJGbyRAnwo+dxivWeUad/mDJ61Ci57LLLdJR36rU7ftw4Y6tADh8+LK++9pqO8kddg9QNSpN++fVX3XLWXytXGi0ya7rK/ZmQoMNVSStXyb4p5gt6KKX69paSnfxnOReA/FGzpPA9JivzKpdbA/GKFc3dyFVJ5gNDh9qz6mrfutqvqqhK0G++/rq9/xyFS8quXbJzxCgRg0WzlICwMIl5fqqElDN/I8YtO3fulLcNHrfYonlze3uLU9QKlReff14iDZ1E8Jp1DVF70p2gbkoUMVinZc3atfZNBactMDQzr6itR82t14TbSNDhmvRDhyXh3qHmz/i0hDeoJ9GPPqxuCeoeAIWZWm4Ycw77COEda9et0y0zqrtU2V/NbN8xcKC9rHzUyJEyePBge98nCpdMfbxs5pEjusecwKJFJMzPTq6Y+e679nngpowZPdrxWiWqbsWtt9yiI2dt275dFi5cqKP8KVq0qHTp0kVHzlNHw6lq7k5S+89NFojr2rVrgaziIUGHK7LS0yXhoUckLWGn7jFHfSBVeX6aBBreVwegYKgjYa6+8koZ/+ST9pE3CdYA5ejhw3LM+jp66JBs37JFfrMGAWr/31133GGfK93m4ovtGUv4nsTERN0yI82FYqWnU3s9Hxs1Sp4YM8bY3lR41/6XX5PkJUt1ZFbGwUOy8/En7f3u/uDkyZPynMG9561atpSOhvZh33P33cb2Mb/l4IoCVTfDpCVLluiWM/bs2SMbN23SkbOCAgPl+r59deQuEnS4IvHDj+XE/EU6MicgOEgqTZ4g4Zx1DPidqKgoefyxx+wq2198/rkMe/BBe+lZhQoV7OWDEdaXmqWsVKmSNLvoIrnrzjvl2WeesYt/fTF7NskQzihu82Z7FsZtvB4Lp+AyUbrljuNzv5NDn3+pI9/2w/z5cuDAAR0575abbzb2vqxmjUsbnX++jpyl6nQkJSXpKH/atW1rf5aaov6tTl5vf7cSfqeW+P9b5cqVpemFF+rIXSToMC55zVrZN26SWoeie8wpqfadX5H/wh4AvEMNl9SxNSuWLZORjzxiJ+rnQg241BJ3+CbTieyChQtlm8HjmoDTRfXsLpHNmurIBdbYa8+TEyTNYGLrllmzZumW89QKq64Gl3erZdLXxcbqyFn79u2TpUudWZVRqlQpu2q5KWofempqqo7yb9Eic5N/3bt3L7AilSToMCrjxAlJGDpcsgzuF/pbWP26Ev3IcDWa0z0AfJ36cLz//vtl1kcfGS3kBe8yfcyWqgZ9ddeukpCQoHsAcwKCg6XSE49LgIvHNmUePSY7R41xZaLElJSUFJnzzTc6cl4z6zpTpkwZHZlxmcHE9+NPPtGt/OvVq5duOe+ElRcsX75cR/mnbrCaoG7Y9L/pJh25jwQdxmRlZMjOkaMlNc7c2YR/CyxWVGJemC6B4eG6B4A/GHTnnTJp4kTHi/bAd1RzobifOkO3WcuW8sGHHzo6uwOcSUTtWlL2vrt15I7j8+bLQR9e6v7rb78ZPVqtk+EzwJW6devqlvNU8TVVhM0J6lg4VSvDFLVVwQm7du82tv+8Zq1acl7t2jpyHwk6zMjKksT3P5RjX5m72/lfgQFScexj7DsH/Eznq66SyZMmGV/iDG9r1KiRbpl18OBB6X/LLdK0eXOZ9ckncvToUf0I4Lxyt/aXsLp1dOSOveMmSuqevTryLV9//bVuOU99xnRo315H5oSHh8v5DRvqyFm7rWT10KFDOsofdTRkG4PHkqrz0J3Yhz537lzdcl732NgCnRggQYcRyes3yL6JU9zZd96zu5S+tquOAPiD0qVLyysvv1xg+7/gHepcYrdu0qhB44YNG+T6fv2kboMGcu/998uyZcvs5bWAk9SKv8qTxkuAi6dLZBw+IgmPjLJXOPoSVQTsx59+0pHz1PFi6ig009R1rGKFCjpy1rFjx+yCl07p37+/bjlv06ZNjqxUMra8PSxMbr75Zh0VDBJ0OC7j+HFJuGewZCWf1D3mhNaqIZVGj2TfOeBH1CDmybFj7bv4QI0a1nU+OlpH7lHHu738yivSqk0badSkiTwwbJhdiMntY9ngv4rUryel+/fTkTuSfvlNDs3+Qke+QS1tX79+vY6cp07/KFmypI7MKmHw+6xZs0a38q/9JZdI8eLFdeSsnbt2yfbt23WUN+qm6dJly3TkrAb16xfIZ87pSNDhrKws2fX4k5K6bYfuMEftO6/68vMSaPA4CADuU/v0buzn7qAV3qUGz7HduumoYGzdtk2efe45ad22rdSoVUv63XSTfDxrll09GcgzNaM6+F4JqRqjO1yQmWlXdU/Zbn6c5hSVzKUavDGm9hqHurSSoVzZsrrlvN8WL9at/FOnpbRs0UJHzpv77be6lTfxCQn5TvKz0+3aawt89R4JOhx1cNancnS2C0VIrDdOxdEjJbxmDd0BwB+o2fOHhw+39+oBf7vn7rs9c1TeXisp/+jjj+WGG2+UKtWqSYtWrWTM2LGy6McfjRaxgn+yl7pPeMLVlYCZx09IwqOjfWapuyqAZlLVGPdukJQrV063nLdnzx7dyr/AwEC5yeCN8vxuWfjhhx90y1mhISFGl/fnFgk6HHNy4ybZM/pJV/adl4i9RkpfV7AzKgCcV7lSJfvuNXC66tWrS4/u3XXkHWrP+vIVK+TJ8ePl8iuvlErWQL9Xnz7ysZXAq8GyE4WQ4P+KtWgupa7vrSN3JC9eIgdmvqcjb1OnLJhUqnRp3fJtcXFxuuWMK664wtjKgpUrV+a5toe6rn5j6Mi9pk2bGqsTcC5I0OGIjKQkib/7fnfOO697nlR+YjT7zgE/1Kd3b3tJM3A6tbJizOjRUqxYMd3jPWrQePLkSZn9+edyw003Sf2GDeWKq66Szz77jJl1nFWFwfdKkOFzuP9t/9Rn5JQPLHVXN8FMenvGDKleq5YrX09Pm6a/q/MOHjokpxwch5coUULatmmjI2eplUjqmLS8UNfTPw29Jq695hr786agkaAj37IyM/+z73zLNt1jTkBEhFR55mnOOwf8kFpaNuC223QE/FPVqlXliTFjPDF4yo0TSUmycNEi6X399VKvQQO7yNyOHTuYVccZhZQuLZWefFytLdY95mUmJcvOh0dKVnq67vEeVZTRyaXbZ6ISvp07d7rypaqtm6LOQVc3CZ2irrU9e/TQkbPU71UV3cyLzZs3y4EDB3TkHPXz3mBdr72ABB35dviLr+Top5/ryCDrjVPxsREScZ75ozAAuK9+/fp2EgZk546BA+Xqq67Ske/Yt3+/XWSuVp060veGG2TFX3/pR4D/V6JjeynWqYOO3JG89E858P6HOvIetQz6FMcc5k5WluOnTJicUf5qzhzdOjfzDO0/v7h1a6nggeXtCgk68kXtO989YpR9UTCteLeuEtW7p44A+JtOnTpx7jlyFBwcLDPeeksuaNJE9/ieTz/7zC4spxJ1dR4w8LcA6/pXeexoCSrlzpFff9s/aaqc3OTs/mWnqPOy87pXubDJyMx0fIa+TJkycvlll+nIWUv++EMy8lCo8Lvvv9ctZ/Xt00e3Ch4JOvIl/t4hkpWSqiNzQuvUtj60HrNn0QH4p+s99OEI71L7Ir/+6itp0rix7vE9apn7J59+Kk2bN7crwCcnJ+tHUNiFlCsrFR59WEfuyDx5UhIefFgyrWTYa1TCefToUR2hIPTp1Uu3nLVv71572f+5OHjwoPy5fLmOnBMREeGpArUk6MiXNJfOO495dqoEFS2qewD4m0rR0VKvXj0dATkrW7aszPvuO7n80kt1j29SBZ1UBfiL27a191UCStQ110jRDu105I5Ta9fJ/ldf15F3qJtZ1G0oWB07dpSQkBAdOeekdf071+0+a9audXSf/d/aXHyx0SPwzhUJOjyv/MMPsu8c8HNNmjQxMgCA/ypZsqR89umnMuT+++0ze32ZGnS2veQSmfvtt7oHhVpggESPGikBLp/9f+DFVyXZStS9JN1Hzmr3Z9HR0XJxq1Y6ctbXX3+tW7mzaNEiIzdsbjR45ntekKDD89JU9U7ungJ+rRrF4ZAHYVYCM+mpp2TWRx9JlcqVda9vSjx4UHr06iXvvf++7kFhFl41RsoPG+Lq1r6slBTZOfIxyXK40Fh+sP3DG/r27atbzjrX5erz58/XLeeobVOm9tnnFQk6PO/gq2/KsZ9/0REAAP90Tdeusuqvv+zZdDXY8lWqINaAgQNl1ief6B4UZmVu6CvhDdzd+nNq9VrZ+8JLOip4xdjemGtqJVFkZKSOnNWhfXv7hqjT1Oqh/fv36yhniYmJsvTPP3XkHHXWe1RUlI68gQQd+RLZoplumZOVmiY7HxwhqbvNnoMJAPBdRa2BvJpN/8sawPXu1cvYQNW09PR0uf2OO2T5ihW6B4VVYGioVJk0QQLC3V3qnvj625K0arWOCpapI778kXqm1EkXJqgjUBs2aKAj56jl6r8tXqyjnC3+/Xf7+ui0/jfdpFveQYKOfKky9SkJKmP+rlPGgUSJH/yAJyuMAgC8o3LlyjJzxgxZuXy53HvPPRJVurR+xHckJSVJ/5tvZnkv7Bo8ZW6/TUfuyFJV3Yc/Yld3L2iqNgn1SXInwOAMupqdv7l/fx0567ffftOtnP3000+65Zzy5crJpZ066cg7SNCRLyHWC7vKM1MkIMTMHbvTnVy6XPY9+4KOAAA4MzXrVq1aNZk6ZYps2rBB3nrjDWnVsqVPzcZt2LhRJkycqCMUWtZrtvydt0torZq6wx2pcZtl74sv66jgqISzSJEiOkJOVBIdGhqqI+d1vvpqCTPw96uZ8dwUfvs1l4n8uVDV29XqK68hQUe+FWvdSqKsDw83JL78uhz71fk3KADAPxUvXlz63XCD/LRokWxct04mjBtnD8pMDDSd9tIrr8i+fft0hMIqMDxcKk94UiQoSPe4Q425Thg4c/pcqH3P4S5Xs/dV5cqWNZqgq2rujRs31pFzVq1aZR85mRN7//myZTpyTu/evXXLWwKyDB4umJWeLpu6xErqxjjdk72oQQMlethQHf2TWta8oVV7yTh0SPecWYO1yyXQR/ecmZT45gzZ88QEHTmrYdwaCQgOtn/X2269Q5J+/lU/Yk5wubJS66vPJMT6s7A6Mn+BJAwYpKMzK9EjVmImn/n3npWRIXGdYyVl4ybdk72S3bpKlWmTdeR/TsXHS9xlne3XsEmBRYpInQXfSYgLW0JMuH/IEHnxJXOFg+6+6y6ZPm2ajrxNfWw2atLEnuE05dtvvpFOHTvqyBlNmzWTVavN7St95+23pW+fPjryNvU7PHbsmHwzd64stBJ3tcRyy9atRvY35tewBx6Q8ePG6chZatBbtUYNuzidKXt37fJcAaZ/W9eynaQfOKAjc6InjZcy3WN1dO52TXhKDr7+to7cEVI1Rs6bM1uCCmh8rd6TdRs0kB07duge56nVNu3attWR7zqvdm15aPhwHZnx0ssvy32DB+vIOQt/+EHatGmjo//1yaefSt8bbtCRM9S551s2bZLw8HDd4x0k6IWAGwm6knbwoMRd3U0y9pv/kIts3VJqvP2aBBTSfUkk6M4hQc8dEvT/R4J+Zr6UoP+bSgLUTLVK2NWXmqlRlYUNDpFyTc1aqZl/E4NIEvT/8JUEPeP4cdl49bWS7nLR3NL9+krlx0fZy+0LwiUdOuS6kFheXHXllfLl55/rCDlR18VqNWtKmsNH8Y146CEZO2aMjv7XgNtvlxkzZ+rIGX1697brlXgRS9zhmBDrA7jylImuLMFKXrxE9r7wshop6x4AAPJGVT6uVKmS3D5ggMz+9FPZsHat/Pzjj3LXHXdI1ZgYY5WRc2Pv3r2yctUqHaEwCypWTCo9aSUxge4O3w9/OEuO/1lwS92bXXSRbpmxes0aycjI0BFyUrZsWWnZooWOnJPTPnR1A3HxkiU6ck6P7t11y3tI0OGo4m0vljJ3ubAf3XoTH3zxVTn+x1LdAQCAM1TRoBbNm8uzzzwj661k/aeFC2XQXXcVSEX4zMxM+frrr3WEwk6Ns0pc01lH7lArzHYOf0QykgrmVIHatWvrlhknTpywt7zg7FShzWu6dtWRc9SKtJSUFB390549e2TLli06ckb58uXtlRNeRYIOx5W/Z5BENDd7t1PJSkuTnfc9IGkuLKkHABRO6oinZs2ayTPTpsnWzZvlpRdekNq1aulH3bHkjz90C4WdOkor+uFhEhTl7s2itB3xsnvi5AJZudjCwIzt6VRyvinu7Ntx8R/XXnut4ydiqJVC27dv19E/qTohTq9w6NK5s9GCevlFgg7HBYaFSswzUySobBndY066lZwnDHtYstJZmgQAMEsd+TTgtttk5YoV8uTYsRIREaEfMUvtiWcJLv4WUrasRI8e6fpS9yMffyLHfjO3Fzw71atVM3rUmlql8sMPP+gIZ6N+H82bNdORc+Zks1LI6RVE6ji63j176sibSNBhRGiFClL56Yn/LSBnUtJPv8q+51/UEQAAZqlZdVUt+fNPP5UiLhSnVUXsfHUJrhcr4/uDkldeIcUudbaQ5NnYS92HjZD0w4d1jzvUlpP69erpyAyVHHqhKKSvMFEQ9EyFAJOSkhzff17RylFat26tI28iQYcxxdtcLKXvuE1HZiWq/ei/swQQAOCeDh06yPBhw3RkjprhU/tknaaK3zm9VPXf2NtrRkBQkFR6/FEJLFFc97gjfd9+2TV+kqtL3YOsn/XSTp10ZMZfK1fKtm3bdISzueLyyx0vnrl8xYr/OQ9dHX+pKsc7qVu3bvb5+l5Ggg5zrA/9ivffIxEtnV8G82/2fvQHHnL9ri4AmKASMiepmSGnj8XBfwom3XbbbcaXuqvf378Hrk5Qg1TTCbrJI9wKu9Dy5aX8g0N05J6jn38pRxf9qCN3XHHFFbplhlrp8ZZHj9zyoho1akjDBg105Ax11OWu3bt19B+LFy92dGWDutnTz+Hz1E0gQYdR6pzyKpMnSlCpkrrHHHUuaMJDI42fZw0Aph0/fly3nPHJp5/K+g0bdAQnlS5Vyt6T6YtMJ+fKZoerL+OfyvTqKRFNL9CRSzIzZddjYyX96FHdYZ46VUEd8WXSjHfesZdU4+zUPu4b+/XTkTPUTZLffvtNR/8xZ84c3XJG1apVpdH55+vIu0jQYVxY5UpSaepT9nIs007MWyD733hbRwDgm5xc0hcfHy/3DR6sI/9y8OBBOXCgYE/yUANVtSfdNDXz47Tw8HAJNJykq2WrMCcgOEgqj39CAiLCdY871KTIzkdH28m6G9Ry6l6GC3up47zGPvGEjgqe0yupnKaOKXO6Evrcb7/Vrf/cqP7lXwl7fl17zTWert7+NxJ0uKLEJe2k1K036cisA1Omy/HFzhaUAAA3rXAoqVH7f/vdeKMkJibqHv+hkvOu114rzVq0sCswF+Rg1vRuXJWcFy9uZq9x48aNdcsMNSNG8S2zImrVlHL3DrK3Frrp2Lffy5H5C3Rknqq8bXrVx6uvvSZr163TUcFQ25Heffdduenmmz1dZLFatWpSvXp1HTlDFYr7+2detWqVoysa1M3UW/r315G3kaDDHdYFteKQ+yS8SSPdYc5/qow+LOmHj+geAHCOGiCWKlVKR2aoY7Xym9SkpKTILbfe6ngFXC9QMyvXxsbaz5Pas9jFStT733KL7P7X/kU3qL3hpm+AhAQHS4kSJXTkrMqVKumWGcv+/NM+4xhmle1/o4TVq6Mjl2Rmya6RoyXNpVUszZs3N17N/YSVEKqbmkddXL7/N3XNX7lypVx6+eVyy4AB8vGsWfLsc8959gaXWjnU7/rrdeQMdeN1165ddlsl607+7OfVri21rS9fQIIO1wRGREjMc1Ml0NAswOnSd+35z/noHl8eBMA3mS4KtnrNGlm7dq2Ozt3JkyflZis5/9Lh/XteoKqZ9+7bV5b88f8nd6gzwj/86CNpfOGF8tSkSUYqnmfnp59/Nn5joEmTJsaW0VcynKCr38XjY8bkeaDNMW25ExgeLlUmPGnX/nFTxsFDsvOxsSq71D3mqJUkQ1zYrrPGuvb26tPH8VogOVFJ6W233y4tL774v8eNqffMqNGjZf78+XbsRdfFxjq6/Ubd8FQ39RSnz6bv0qWL45XnTSFBh6vCKleWSlMm6MisE/MXyYF33tMRADjH1Gzm6cZPnJinpEYN9GK7d7cLw/kbNWDue8MNMi+bgduRI0fk0ccekzr168u06dONz2yr/e+Dh5ivon3hhRfqlvNat2qlW+bMfO89eeONN87p9bxp0yYZPHSotGvfnkrwuRTZoL5E3eLOdsLTHZ83Xw598ZWOzFIJYaXoaB2Zs2DhQmnTrp2sW79e95ixdds2GTZ8uNRr0EBmvvvu/9yQUq99tTpo+/btusdb1BL3enXr6sgZ38+bZ2/P+vHnn3WPM26znkdfQYIO15W8rJNE3TlAR2btnzRVklat1hEAOMPp42XORCXYL738cq6TGjXz8N7778tFzZvL/AXu7Qt1i1qyP2DgQPn2u+90T/ZUkb3hDz8sNWvXtgvkLV261PFj5rZZA+bOXbvaA2yT1L5Jk8WxatWqZX8Pk9Rzf89999mJxurVq8+YcKvX70YrKX/nnXfkyquvlkYXXCAvvPiivY1BJS7IhYAAqXD/PRJavarucIl1jdoz7ilJdWErQ7FixeTRkSN1ZJZKzlu2bm2vyjns4DG+aoWTmhVXK4HqN2wo0599Vk7mcIzi/gMH5NrrrnN1ZVBuqZU9vXv10pEzVN0Kdc12sq7IBdb1RB0N5ytI0FEgKgy+T8IamN1HpGRZF8GE+x7gfHQAjlLFcUxTifnQBx6QIUOH2rMnahn3v6k+dXasKijUtFkzueW22yTx4EH9qP9QyZv62T6bPVv35E6y9RmgbnK0bd9e6jZoII89/rgssxI+NTuTl9UJasCoKj1PfOopaXLhhbLir7/0I+ZUrlxZzrcG8aaoGbCSJc0fhZphPXcffPihNG/VSirFxNhJuNqG0cdKUlpdfLFUrlpVLrCe09sGDrRvMJ3+ele/N7U3FWenlrpXGjdWrQfXPe7IOHRIdo4cLVkZ5rcWXn/99UbfE6dTybRalVO3fn15cPhwWb58+Tknyuq1rFbzqO0walVInXr15KouXezr2Zmu62eybt06ueOuuzxZ2V0l6E7e5NuwcaN89vnnebpGZ0dVbzd9I9JJAdYP79xP/y+qWNemLrGSujFO92QvatBAiR42VEf/lJmaKhtatbff/DlpsHa5BEZG6gh/S3xzhux5wsyy8oZxayQgj/s5Tm3bLlu6XieZScm6x5yiV14m1Z6f7spRb25QVVMTBgzS0ZmV6BErMZPP/HvPsj4Q4jrHSsrGTboneyW7dZUq0ybryP+cio+XuMs6Gz8/P7BIEamz4DsJKROle3zL/UOGyIsvvaQj591tDTymT5umI+/7a+VKad6ypaMDiJyo47BqWImUOgv472RKLef+fckS2blrl6t7JbPzzttvS98+fXTkHDVzrhI5p5bsqyJ/ZaKi7JssHTt0kPPPP98uPFW6dGm7UrraT6m+1MBZfamZM1WI7pdffpHvvv9e/szDAD0/Hn3kERltJQgmXdutm3xz2vFGXjT4vvtk8qRJOnLWupbtJN2FQmfRk8ZLme6xOjLIui4lPDZGDr//ke5wifXeqvTUOIly4Wf82Xo/qmJqBZGwli9f3i441rJFC6lQsaJ9bVbXjrDQUEm3rhlqmbq6qaqKI8bFxcmvixfLgf375eixY/pvyLunJkyQoS5sqzkX6nfQuk0b+9rolL+vwU5Q1/y1q1b5TIE4hRl0FJjw6tUk2rqQiwt3tE58O08OzJipIwDIHzU4i7CSZreoGWS13PKtGTNk2jPP2F+qvX7DBk8k56aoga7a4+3kfnp1U+VAYqK9dPqpyZOl3003yYXNmkm1mjWldNmyUrVGDXvZaYyVwKu45nnn2fugH3n0Ufnxp59cTc7VTYN777lHR+b0MXBjxWkvvfKKbLKSHeSCWuo++F4JKldWd7jEem/tnThZUvft0x3mXNy6dYEdmaVWLakbBJOffloeePBBu+ZHp8sukzaXXCLtO3a0bxyo7Thq5n3GzJmyefNmR5JzZfTjj9v7471EzUx369ZNR85wKjlXLmjSxKeSc4UEHQWq1FVXSKm+zu5dyc7+ydMleW3Bnm0JwD9ERkZKhw4ddAQTVHJ+/+DB8vqbb+oed6iVCfEJCY4NqPNj0J132km6aZd26iTFixXTkTeplRQPPfywa6tWfF1IVJRUGjPKlUmQ02UcOiwJwx8xvyrN+rmenjLF8QJlXnfKeh/c1L+/7NixQ/d4w5WXX27PVHvRDQ4fBecGEnQULOsCGz1qhISfb77gUtapU5Jw71DJOO69IhsAfE/PHj10C05TSyYfHDZMXn39dd1T+DSoX18efughHZlVpkwZueyyy3TkXV9/840s+vFHHeFsSlzaSYpd3klH7kn6dbEcnGX+FIkiRYrIzBkz7MJxhcm+/fvtWXsvrZ5q1KiRJ4uwhYaGSteuXXXkO0jQUeACw8Ik5oVnJLBYUd1jTuq27ZLw0Eh7DzYA5IeadVQDRDhLLW18ctw4efHll3VP4aMGla9aP3+Y9fnoBjXz9cjDDzt6nrEJavZ8+EMPcexaLgUEBkrlsaMlKMr8Kox/sH5Pe8ZPklM74nWHOY0bN5Y3X3/dfs8UJqvXrJFB99zj6FLw/FArGkzUIMmvphdeKNWqunyqgQNI0OEJYVUqS/STj9sz6qYd/26eHPzwYx0BQN6oQkGxDu+7My0qKkouaddOR96jErBJkyfLuAkTCu1SZpUkPzNtmjRv3lz3uEMVy+vapYuOvEsVaHxnJjVlckstda/w0IP2vnQ3ZSUny87hIyTLhSJuqkL3+Cef9OwSa1M+/OgjmfL00zoqeF07d7YTdS/pf+ONPvm6IEGHZ5Tq2llK9rpORwZZHxZ7x0+S5HXrdQcA5M2wBx7wmZkb9e987eWX7UrwXqUGUl2sQV6tmjV1T+GiBrcPDx8ut916q+5xj3ruxz7+uGuz9vnxxLhxdq0A5E5U7LVSpO3FOnJP8rLlcuCtGToyR712VTHFxw2fduBFU6dP98wRhOomX6XoaB0VvKJFi8pVV12lI99Cgg7vsC6w0SNHSFgd85UWs5JPSvxd90mGH1c/BmBevXr17Dv0vkANXtVevJiYGN3jTWqQ9/vixfbZuoVpRiw4OFhGPPSQPDZqVIH93Or1rI518/rzvnv3bhk/caKOcFaBgVJp9EgJiIjQHe7Z/+yLkhJvfqm7urk14uGH5ZWXXpLIAvg5C4Javr1w/nx7ZZQXhISEyI39+umo4F3UtKlUrFhRR76FBB2eElS0iMS8+KwEFjdf8CMtPkF2jhxt75UCgLxQicyTTzwhVSpX1j3eo/6NI0eMkAcfeMCO65x3nv2nlxUrWtQu/vTBu+9KxQoVdK//UufcPzt9un3eeUEvEX1g6FDp0L69jrzrRSsR27hxo45wNuHVqkn5YUPsyRA3ZZ44IfFDh0umC3UD1LXu1ltukdmffSZly7p8xJyLSpQoIU+OHSs/LVok9evV073ecF1srGdqWVzft6/nbzZmhwQdnhNeo7pUfMJKnF0YpBybM1cSP/hIRwBw7tQxWK+/+qonl7qrWVk1I3r6rGy5cuXsP71O/Xu7d+8ufy5dKjf16+cTS6/zomrVqvLNnDly+4ABnhhMqlmw92bOlEbnn697vMk+dm3EiEJbqyAvyvTpLeEN6+vIPSf/WiUH3npHR+Z17NBBfv/1V/usdF9N0M5EJb6XXXqp/PnHH/LQ8OGe/MxRq3C8MGutKvur2gS+igQdnlSqy9VSsqcL+9GtD/Z9456Sk3GbdQcAnLuOHTvK1ClTCnz283RqmecLzz4rj44c+Y9/V6lSpXxq0Kpmwl5/7TX5+ccfpXWrVp56jvNDJcK39O8vS377Tdq2aaN7vUEdu/bF7NmeT9LVsWvz5s3TEc4mMCxUqjw1XgLcvtlljbUOPP+Sq2MttZXnu7lzZdwTT9h7kX2Zul43aNBAvvjsM5nz5Zf2TT2vUjcNenbvrqOCo66p6rPOV5Ggw5PU0SDqfPSwuuaXYmaq/eiD7peMpGTdAwDnbuDtt9uVhL2QQKol93O++kpuvfXW//n3qJkFVYHel6gB6gVNmsiCH36QL63EUSXqvkztjfzeSh5eefllz+wf/bfK1mtIJThqNtKL1GtCnUhQycPbS7wo4rzaUmag+0UIM5OTJWHYw5KZlqZ7zFOrboY9+KD8sXixdLvmGp+cTa9bp468/cYb9o28K664widuUHrhuDVfr2FCgg7PCipSRKpMmyyBLpwznLp5i+x6bIxd4R0A8kINBtT+3bdef93eQ10Q1L+h3/XX28vCs5uVVTMcBfXvyy+1xFMNUhctWCAL5s2TXj17+sxZ9Op3oxLz9999V379+WdpY/1+vD6AVDPpasZOFa8L99AWA1Uc64P33pPvv/1WGtR3f8m2T7Nec+XvulPCrETdbadWr5V9z72oI/fUrl1bZn38sfy4cKFccfnlnj/vX1HL8z98/31Z8eefcr11TfelLT5qmXv16tV15L7ixYv7xJGROSFBh6dF1K0jFceOsl6p5l+qRz//Sg7O/kJHAJA3ajC1dMkSV88bV3vN27VtKz8vWiRvvvFGjkv71NJqNYvuy1Ri29b6edVe6a1xcfLUxIly4QUX2M+D16iCTj2uu05+XLDATsx79ujhU8v01etl7JgxsmTxYunUsWOBPceqkJ76/p998on9PHa3nlN/2e7gNrXUvdK4MerCoXvck/jqG5K8vmCOuW3VsqV89cUXslzXtSjjsdUrau+2OmLxLyspV6uF1Gvci9e0s1Hv1dhu3XTkPnWd8PnPuCyD1TWy0tNlU5dYSd0Yp3uyFzVooEQPG6qjf1KVHze0ai8Zhw7pnjNrsHa5BEZG6gh/S3xzhux5YoKOnNUwbo0EGL54ZGVmSsKIR+Xox5/pHnMCrIFIza8+kYg6dXSPNx2Zv0ASBgzS0ZmV6BErMZPP/HvPysiQuM6xkrJxk+7JXsluXe2VDP4q7cAB2TVuovWcmF09YQ+IHntUgl04ocCEGe+8I/OsAYMpl3bqJDf3768j/5Bhvc++mjNHxowdK+s3bLBjpxWxPvNUovrIww9L8+bNcz0zpCpg/2YlXE4adOed0rp1ax25Tz2/O+Lj5QtrAP659bV23To5evSoftQ96uZByZIlpdlFF9mJeTdroOrLeyFPl2l9Hq9YscI+4mzBwoVy4sQJ/YgZatawRo0a9p7W/jfdZC+7N5GUJ4wcLenHjunInKjr+0jxVi10VPD2v/m2JK1YqSP3hJ9XWyrec5c9m1+Q1PVh7ty5MmPmTPlz+XI5fPiwfsQd6nqtiox2uOQSOzFv2bKlRPpJHqM+88aNH6+j/3UyOVm+tp57E5+L6satWl3ly0jQCwFfT9CVDOuDc/N1vSV1yzbdY05Y7VpS68tPJDA8XPd4Dwk64DvS0tLkj6VL5ZVXXpG5334rx62kJq+DkkBrQKtmNFUyrgYgna++2k5avL5U2m1qaHPw4EFZvXq1fPvdd/bge8kff0i6NS5RX05SM1xqoK0S8hbW7+Vq63eiiqupJN2fqbPIv7EG2LM++cR+bk+ePGkn8PmhXttqxUFrK1FRS1TbtWtnF8TyhSXJ8G1HjhyxrxOq8ODixYtl9Zo19rXCyQRSXSvUlhx1rbj8ssukffv29h7ziEJybvvpPps9W3r37asj56jnd1d8vM9sfcoOCXoh4A8JupK8br1s7XmDZCWbL+ZWsncPqTLhiQK/u5sdEnTAN506dUrWWAM/lbD//vvvEp+QIIlWIhlvDShUgnM6tf+3fLlydhExVfStWbNm0rBhQ2ncqJHfJ38mpFpjiZ07d8qatWvtPzfFxcnWrVvtgbmaSUtKSrJn4M9E/Q6iSpe2n3e1v7GalTTWq1tXqsTE2L+TypUqFcpB9t/Uc7fWel5Xrlol69evt2fP1Gykel63bNki/x5oVoqOtmcO1Zc65/6CCy6QmjVrSiPrtR1TpQoJOQqcWh2ybds2eyWOul6om1DHjh2zv9Q1Y/eePZJ8hvGoSgyjK1a0bzSp64W6gapu2Kmq8udb14oq1utb3YgqzNRnXYvWre1rhdP63XCDvPXGGzryXSTohYC/JOiKOrN8zyOjdWRWpWcmS+lruurIW0jQAf9xto9hZsfNy+1QiN/FucnpeeW5hK/KzfWC13f2Xnv9dRl0zz06co66sffDd9/ZBTh9HQl6IXBk9hdy4OXXdeSsWl9/biXoLt7ptl6uu6c9Kymbzv6ayq/AYsWk8phREuTB1xQJOgAAAHzJvn37pPEFF8jBs+R0eaG2C/y1fLlfrMAhQQd8EAk6AAAAfIVKOW+59VZ574MPdI9z1IqF1155xS4m6Q84nwIAAAAAYMz7VmJuIjlXypUrJ9fFxurI95GgAwAAAACM+PXXX43sO//bnQMH+vzZ56cjQQcAAAAAOG7V6tXSq0+fM1a9d0LFihXlvnvv1ZF/IEEHAAAAADhq/oIFcvkVV8j+Awd0j/OGP/igffylPyFBBwAAAAA4Ii0tTZ5/4QW5NjbWSMX2v9WtW1cG3n67jvwHCToAAAAAIF9Upfb169fL1V26yJAHHpCUlBT9iPNU5fYpkyZJaGio7vEfJOgAAAAAgDzbsGGD3DlokFzYrJks+vFH3WtOrx495IrLL9eRfyFBBwAAAACck0OHDsnszz+Xzl26SKMLLpA333pL0tPT9aPmRFesKM9Mn64j/0OCDgAAAADIUVJSkqxbt07eevttib3uOqleq5Zdof37H36wl7e7ITw8XD547z2JiorSPf6HBB0AAAAAIJmZmXLq1Cl7dnzDxo3y1Zw5Muqxx+TyK6+UWnXq2EvYB955p8z55htjR6dlJzAwUB579FFp3bq17vFPJOgAAAAAUMidOHFCWrdpI42aNJEatWvL+Y0by3U9esjESZNk4aJFkpiYKBkZGfq/dl+fXr1k6JAhOvJfJOgAAAAAUMgVKVJEdu7aJdu2b7eXs3tJ2zZt5OWXXpKgoCDd479I0AEAAACgkFNHl7Vu1UpH3tH0wgvl01mzJCIiQvf4NxJ0AAAAAIDUOe883fKGi1u3lrnffCOlSpXSPf6PBB0AAAAAIM2aNdOtgqVm86/p0kW+njNHSpUsqXsLBxJ0AAAAAIBUqVxZtwqOSs6H3H+/fPjBB1IkMlL3Fh4k6AAAAAAAiYmJkWLFiunIfSVKlJB3Z8yQpyZOlJCQEN1buJCgAwAAAACkePHiEh4WpiP3BAYEyGWXXirLly6VXr166d7CiQQdAAAAAGDPWru9Dz26YkV56cUX5asvvrBn8As7EnQAAAAAgK1Bgwa6ZVZkRITcPWiQrFyxQm695ZZCccZ5bpCgAwAAAABsFzZpoltmhIeHyx0DB8rKv/6S6VOnSslCVqX9bEjQAQAAAAC2OnXq6JazqsbEyNjHH5fNGzfK888+K9WqVtWP4HQk6AAAAAAAW3R0tBQtUkRH+VPJ+rv6XX+9fD93rmxcv15GPPywlC9fXj+KMyFBBwAAAADYihYtKuXymESr/2+d886TO++4QxbNny8b1q2Tt958Uzp06MAe81wiQQcAAAAA2MLCwiSmShUdnVlAQIBd5E3tH29/ySVy3733ytyvv5YNa9faRd+ee+YZufjii+395jg3JOgAAAAAgP9q2aKF/We5smWlcePG0rFDB7kuNtZeoj5zxgyZP2+erLeS8V3x8TLvu+/k6cmT5dJOnezl68yU5w8JOgAAAADgv0Y9+qiknToluxISZNmSJfLd3Lny0Qcf2EXe+vTuLW3btLH3qoeGhur/B5xCgg4AAAAA+C8S74JDgg4AAAAAgAeQoAMAAAAA4AEk6AAAAAAAeAAJOgAAAAAAHuAjCXqA/b+zyTx1SrcA/5aZfFK3chDE/TcAAADAl/jECD4wNESCihXTUfaSVq3RLcC/nVy2XLeyFxJVRrcAAAAA+AKfmWILv6CxbmXv6NdzdQvwX1lpaXLsh/k6yl5o5WjdAgAAAOALfCZBjzy/oW5l78S8BZJ+6JCOAP907JdfJX3PPh1lL7JFM90CAAAA4At8JkEvenEr61+b80b0jKNHZfdTT4tkZuoewL9kJCfL3vGTRLKydM+ZBZUvJ+HVqukIAAAAgC/wmQQ9rFpVCa1eXUfZO/rp55L4yWc6AvxHVnq67Bo5WlI3b9U92SveqYMEBPrM2xsAAACAxWdG8IGhoVKqV3cd5SAjQ/aOfFwOvPOuZFltwB9kJCVJ/IMPy9Ev5uieHFiJeanePXQAAAAAwFf41BRbVN/eElSqpI6yp2Ya9z4+Trbffpec2rqNJe/wWeq1fOynX2TztT3lmErOz7K0XYlscZEUyUXNBgAAAADeEpBl0W3HqeRiU5dYSd0Yp3uyFzVooEQPG6qj7O1/5XXZN3GKjs4uIDhYIpo1laLt20lErRoSVLasfgTwqKxMSYvfJSc3bpTj3/8gKZs26wfOLiA0RGp8+oFENsw5QVerS+I6x0rKxk26J3slu3WVKtMm6wgAAACAKT6XoGempsnm2J6Ssm6D7gHwt1I3XS+Vxzymo+yRoAMAAADe43NVpAJDQ6TK1EkSWKSI7gGghNWvK9EjhusIAAAAgK8xm6AHBFj/y/lotP9KT9eNs4uoc55Umj5JAkJCdA9QuAVXKC/VXn9JAsPDdc9ZqHUzuVw8o7aJAAAAADDPeIIeGJm7me7cHB11upKdOkrF8WPsPbdAYRYUVVqqvf2ahFasqHvOListVTKOH9dRzoKrV9UtAAAAACYZTdDVOczBuai6rpzasUO3cslK/qN6XCdVXnlBAosX051A4RJap7bU+OR9e1XJuUg/dFjS9x/QUc5CKlTQLQAAAAAmGd+DHnZ+fd3KWdqWbZKyc6eOcq9E+3ZS68tPJKJ5U90D+D+17Lxk315S67OPJLxaNd2be8cX/y6SkaGjHAQESFiVyjoAAAAAYJLxBD3ivNzP7B3++DPdOjdhVatKzfffkehJ4ySkahXdC/ihoCCJaHahVP/4XakybowERUbqB3JPVXA//PEnOspZYES4hFU/9xsAAAAAAM6d0WPWlLTEg7KhRVuRzEzdk72QypXkvO/nWElBhO45d5kpKXLsx5/l4DvvyqnVayXzWO722QKepbaKRJWWyNYtpcyAmyWyXj0JsBL1vEpatVq2de9rH4N4NqE1qkudH76xZ9IBAAAAmGU8QVc2XdtDUlat0VHOyg65Vyrcd7eO8if9yBE5uXGTJC9fISmbNkv6sWOSlZqmHwW8KygyQoKKF5fwCxpLkSaNJaxaNbsvv1RSvqXvTXJy2XLdk7PSt/WXSo+O0BEAAAAAk1xJ0Pe9/Jrsf+ppHeUsMDJSqn/ynj1LCMBZB955T/Y+/mTujlgLCpSaX34qkfV5LwIAAABuML4HXSnZ+apcL8nNTE6W+Dvvy1PBOADZO7rwR9k3bmKuzz8Pq1VTIs6rrSMAAAAAprmSoKsq0EWvvkJHZ5cWnyBb+9woJzdv0T0A8sxKyI98+70k3HXvOW3xiLrlJrtaPAAAAAB3uJKgK+XuvP2cBvvpu/bI1tjecujzL3NVzArA/8o4cUJ2TZgkCfcMkayUVN17diHVqkqp2Gt1BAAAAMANriXokfXqSvHYrjrKnUyVXAx9SLbccLOcWLqMRB3IpcxTp+Tgp7Ml7oqucui1t3J35vnfAgKk3H2DJDA0VHcAAAAAcIMrReL+lnYgUeI6d5MM689zZiUNoTVrSNF2F0uRC5rY7aASJfSDQCGXlSnp+w/IqU1xkrRkqZz4dbFkWHFeFLHeY9Xfek0CAl27fwcAAADA4mqCrhz5YYG9F1bSz2FGLzuczQz8PwfeyoElikutObMlrHIl3QMAAADALa4n6CqJ2DPpaUl8+XXdAcALAsJCJebVF6V4uza6BwAAAICb3F/DGhAgFR4cIiWuowAV4BlBgVJh9EiScwAAAKAAFcgmU3UmeuVxY6TopR10D4ACExgo5R4YLGX69NIdAAAAAAqC+0vcT5OVmio7HxsrRz76RPcAcJNa1l5xzCiJ6t1T9wAAAAAoKAWaoNusb5/43geyb8IUyUxO1p0ATAuJqSyVp0yUos0u0j0AAAAAClLBJ+jaqe3bZeeDI+Tk8hVW0q47ATguIDRUSlzbRaIfe0SCihbVvQAAAAAKmmcSdCUrPV2OfP+D7Js8TdJ2xNuz6wCcERASLBFNGttL2iPr1rE6OKYQAAAA8BJPJeh/y0xJkeO/LpbEN96Wk38ssxN3AHlgJeGBRSKl2GWdpMytN0lk/fp2UTgAAAAA3uPJBP10qfv3y/FFP0nSb7/LyY2bJG3LNslKS9OPAvi3ACshD6tVUyIbny9F27aRoq1aSFCRIvpRAAAAAF7l+QT9H6x/qppNTzt8RDJOHJf0g4ckKzNTPwgUXoHhYRJUvIQElywpwSWKSwCz5AAAAIDP8a0EHQAAAAAAP8U0GwAAAAAAHkCCDgAAAACAB5CgAwAAAADgASToAAAAAAB4AAk6AAAAAAAeQIIOAAAAAIAHkKADAAAAAOABJOgAAAAAAHgACToAAAAAAB5Agg4AAAAAgAeQoAMAAAAA4AEk6AAAAAAAeAAJOgAAAAAAHkCCDgAAAACAB5CgAwAAAADgASToAAAAAAB4AAk6AAAAAAAFTuT/AEi4PhsWDpChAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,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" + }, + "f8a011f3-8c0a-4d15-8006-17111f9edc7d": { + "name": "Security Key by Yubico", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "8976631b-d4a0-427f-5773-0ec71c9e0279": { + "name": "Solo Tap Secp256R1 FIDO2 CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAMAAAAKE/YAAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAC+lBMVEX////w8PDX19e+vb2lpKSko6O/vr7a2dn19PX6+vq7urp6eHhfXFxGQkMsKSojHyAzLzBNSktoZWaKiIjS0dLY19iDgYH8+/zZ2Nl4dncxLS6XlZW6ubn4+Pjo5+d4dXYlISI5NTaurK3+/v64t7csKClZVlfv7++joaHk5OQ5Njfr6+vg3+BlYmJWU1SopqfHxsYmIyM9OTpST1A/PD04NDV8eXrW1dX8/Pze3t6HhYUtKiq8ursvKyzj4+Pv7u5fXF1nZGXR0NEnIyTh4OD09PQrJyhaV1jm5uZ+fH1EQEHFxMTKycq3tbaioKGNi4y2tLXu7e7GxcWxsLCenJyRj5CmpaXQz8+Rj48/OzzEw8SWlJRVUlMmIiNTUFGUkpP9/f3Ix8eIhoZHREVkYWKkoqKenZ3U09NhXl/T0tJKR0d7eXkkICGCgIBsampraWnV1NQqJidraGnl5eW0s7NXVFTs7OxFQUL29vY+Ojt2c3QoJCVcWVqamJnMy8vNzMybmZo6Nzjn5uc3MzTp6elYVVX7+/tmZGRiX2DOzc1STk+Vk5OPjY3q6uo0MTFta2uBf39MSUqGhIVeW1vLysuwr6+qqKi3trY1MTLy8vLj4uJbWFnKyclCPz8pJSaqqalIRUbc3Nysq6uysbGzsrJ1cnPf3t8zMDEuKiuZl5ihn6Ccmpr29fXJyMhPTE2LiIn39/ddWls8ODlzcXFycHCAfn5UUVKXlpZLR0h0cnJYVVa5uLhDQECQjo6fnZ5JRkZxbm9jYGEwLC1MSEllY2Pz8/NBPj9RTk7b2trDwsJQTU2pp6hwbW5OS0yLiYpgXV7Pzs75+flqZ2gyLi87ODjCwcGdm5uJh4erqqpAPT6npabQ0NCEgYJ+e3zx8fGtrKzAv79yb3CFg4SSkJFua2y1s7S9u7ywrq/DwsOMiouEgoPc29uYlpe9vL19envt7e3d3d02MjOvra7p6Oignp9pZmd3dHXBwMDi4eFGQ0R/fX6OjIxvbG3W1tac12V4AAAAAWJLR0QAiAUdSAAAAAd0SU1FB+IJGhc6HI0t8mAAAA2TSURBVHja7Vx5fBRFFi7CHUkaRAy3wUC4xJAAS7jCEQgokVPkTBiyikCGy4UVCUHOoIaQcCcYgsgpyxFAETcCIgRw5UgMuAroxgtWFPBYV113f7/N1OueetVd3TM1ESZ/9PdPpt5R/aW7uvpV1asixIYNGzZs2LBhw4YNGzZs2LBhw4YNGzZsSKNSQOUqVatVr+FvHl6iZuA9tYKCFRW169xb9z5fq6p3P0PIHaRcv0FDxYCgRr7d8caojiZ3jHLTB0IVIZo9GFZRSTdvoZgivGXFJN0qVLFAUOuKSLqKYo02bSse6YdaeCCttKtwpMMe9sRZUSIqGun2OoKRUR06RupknSQ72ztO+gHMLvgPnaPLZCFdunbjWHevWKSb9EAXiIpxy3v2wqR7VyzSfVD9sX2Rol8dpImT+8TcadKBqP7+nKYevtUDKhTpqqj+R3jVo0g10OjZMv6xQYMHDxoSP1SS9IBhwx+vO+KJwJE+/z+jUP2jeVVEb4YxOreAseMSNLfQxPGdvSXtmJD0R9bonnxK7glqmIgbwWNeOj09Sd+T15rsFenuU/QdbHJTH0g3x1U4p3rzxNpOcyoGOKejj70J6RmJRj9lZlJNadJ9+CoaPhPxJw8enaMUIaJYGxGTnmUSL8z+syzpGsaanp1abY65Q+NgxQTBjS1JDzbzU56rL8t6rqialHmp9cTm82NNr62kPG9BeoG5n7JQNo6cb1ZTmweGVDJYL1pscW2l2RJT0gMTrByXpkmyXmZeV8ILL/K2jpewuluv9OXhM7FkdpgJ6YwV2KxT5uNZK7mRxypJ0pVMXizA6jXYdi3SRK6jsV/NVNyXrDch/QiSZMOdyJmOZLEbJFnft0Kxwsu5bsuQjUycF6hJN6En/4pDSHoDehMWblb9ohsgs7mSpEnrlZaslfGa4atIuIX54w/UViHpbegBbWeO9zJxwkOyrOeM2GHJOtkBdihcjYpG7mjKpLeIdNpOVs5E130R2b0mS7rsurtGW7H+CzXancckjbD3KibfmSYgvQeVuXdkL5Ovlidd1l6HWzSSvOouk+7oaXJfsb7IdI+A9D5WnMJddB26RL4vrAmJiZhe24T1fpc+iZUP8J7o8acLSM9mxYOc3wxkON830mVw9El/eaaAtNMVQ77Oyom8WxDTvCEgjTqdfZzfUGS43mfSLjRpv/yQIY57s0xRixWf4V32M800AWn0IAbxjnFM81S5SLvQOj2IJ+0aih1mxam8+VtM81cj6XxULOAd32aaI+UmXYajXGj0Nt8Iknjbe/iGoyOdg4rVeMdjZg3HV8zHjbtFmSCcFd/hTY8zTW8jaYK6St1k1btMM9FbXtF1TjDs0WtP4ltdSEgm3wgQUMNJFpBG0Q3fCPohwy3EWyxEXll65SakdJYNirJY8RRviT6oywWkT7NiA87vDDIc5jXppciro145HCk7ES704D8FLZFhgYB0Misu5a5QgO7KUOIt0GuvKO/plKhfVv5WVm6LOsJN2DCVyWMLBaRR2dkFO6J3Ya/XnMn7mHTD6pwuBn8ezxL+MZ9Dhg4Ut4QTAel+qCPKQo590V047z3pHO7zF4Wjmc6dsIoOWhshARrTYI4TRaTJBVbuUcgc70d2Rd6Txj2CC3Ve3VDsEs8p+CAPy2vTyYmcEia5eEarogg9kezdQtJ4IDo7R3OsgkZc8yQ4k1zFgBWHn31XL1Mf6lgk2jESZJfwnMKHREgaN15lpRohjscXkAuXkhUvsFhdl6uBm0xk4t8rN7//HB6gXsw3IT0DD8Z3TmrU/qO5H+MLPCnFmfSzHNeqcE/yxcdamaUUERPS5EPL+i/KTjKNLFE8AX0RqlrZXSampMlZC7+8K5KcCanfxgPnq3gdIMnczh1FiUjP6W/+gLZKcy7rkM9ZUY5sxFtHmLSQWBYLCefy0j4xuUD2Gq+ZYjgisk05jwvQW+ceENkdYNMjZlO9T+wUOXaQX8ZW8ekR8Wj83D8ES0TFuzrp7RYfLUYGZpPqPZMMc7RTGnuiZoWw+OTndBWeWmU2B5t/+SS6fNyTVXZz6pFo4YOfWsx4cynq/LIPNvYlM4NHy4EL7smc9PCUOv17bxtV2tPStvhS6qrP9u//7PPUUrkFn0pDxmZlhk+au+/oSEe5GduwYcOGDRs2bNiwYcNGhcXlcBe+MNFuodrw/r6vTN4R1KVDzC/Fyq3qKHSXv1lKkP5K5dzK3yQlSK+HPGpnVX9zlCBdoHJ+wt8UJUgHwpyd831/M5QgfQ04h27yoU5/ka6cApxf9Tc/CdKlsEwU+qC/6UmQvgScE677m50E6X/C6mLCcH+TkyA9EPJdEnxZVfAX6fbAOfIrf1OTIL0HpssjTXPtw9YkTR83us3edslr0ZIxcTRxQZyeW0x1rDxg2Lqvz447njXxWvX834N0LizAxjY3sc+4gXJE8k6yHQ7fUEmUQ+CziC6QulPy4lEGlxJ8vhKRho70Gtj/FGuyFBJ9FO9AcuF1d54G5I6MEXh9i0PFCeG6GhqO3U0kwZN+HjinmGzWytirGLBDi7UhT/kdgRvdJRL3Kf1dWbBjM0p2wZYjXQSLZik3xbYxp7RmcfpW0oVmamGnmkVRTJOC4nIMbpOpGeQ+dlFzBfLerrWt3WEts3ZeNJECJj0Snn1eNbHpBmjNoec7w+t2+zokTfSYAfrPackYFEJaR7zrZyGkyY2+rO4TubIM8lS+9pl0H7gLeaViy+hDVL0QZZU1nUdFh2G/4ne00EHvF/K9SxxEf/9ATWajPmYPDcyc7xEZMNKT1YeVMkNsOYJqe3ErdQ5wh1RlAsvf3+j8biITetNLfsTqf1F1JpGBm/TT7myER4Vv8xk6Jvj+U91tpC9Ztwxa2ErdddmRZBq9E9DJ0L2xP/H6Di5ZbYcvpDujpJ5tIsN/U9UPevF7VAyL/jXpErtucyukScFL46AfgRF8DV/QGqSyJ1TSAVyCvSBSWkID7HCjop1LvhF+Q14F3/dEUBnsDQyh/d1ZvgJIsh9PJACkz8EOjLyxMC7c2ddgd8TsflyiCshBeIj2BR9weprxfUpdA6fd5Pf8gnjIVhekZlbqohuc97OWWnXaEEPQbTklDmMFbXFDponUsTiZ8Rcnaz6EQAc0VbJbtiLt6usc0IkZ3qZCOgUi3CC8GLWbIdT5KNLSFhuZoZbUHVzHq5NygZGGb8oSyFfRd5zXqPRxUQ10I0k3eAZp9D84gbQbuf4iQ8v2O5Z+RXa/loh0SmUQVINv1GI+HoDkx0ttBbhFVeq920cLM9x+z9NyqbuMDl6YOW5Vwe3ykdY4E3IDBBe41+Wq4gEqL2jCWW4/+h/hePVz3u3X5OvWeSVWpFGMVFPNw1qAzT7zRFobm9HGskPbglpcYuiYtzTTebb4pAuRBJBOuYZE29WYGp9Zc8ETaS1Ogk272rBnvauQsIi7YtqspTpf57IAIgUgzX/6IaxRTvVjopOeSGt7r0LojTyuluhmR2NOZkBSIp8oF3yNyEA473EQqnqdSeiu1tCYDFO445XB9ObCHtChlFqg6Lr5E8b3QqdEJLxIJCAkXUPdA8QmmGBPmTeHHLWmn+pv6e9Brp/NTA/aCLmSWkvL++4oM+YST4tNhqm8bu7Ng/BV8Op0khdclhA+09R26wD/l6QS/Q3ylbSWhXtO6wbW0OIn3tQIZ0K4opTt9C3ztBN1M6QmymQjm5AOewFY31DLNekMTqI3NUbTUdlVoqZ11/LosJm2/B3lJ01uQ3fqLFXLNCZJEd21WRPLgIeVNCBs4yCEnnwwhCn+434GPGCMX0y8hulKwEAY62ersQ4kTk8z2v1Io1m8XjCABlcTYPomGx11QN9L5TdDFZDvK5Eoa77mch4ayGr4nM+B98WYNvwb/ar1wyI6LkiGQWVXJB9DqzhhqAICB4k4xJx0CAS/dCui2/C0PqN1Nx1rv8XJ6FC2dtqvrj/4E53fTXxL6RcyViJX1mJJLgamFCJhm0UGDMh0HVga7HCewAkdNMOaTobx4zPYo3RIdz7EADrlecx7zpaLn0PUfh8mR9Ws6Kv4W+H4ksp+1d0lGvnTlr2Wk6v7XY5zn5ti2KiU/juR1jZH/hdK6u6SY+7bGrb+BJWs2K7za6olSZfo0pTVMy7mXWL/5ZqXqWimp3NFvCadrx4wA+tyxdpZDx933TLhfz9XqfsKFOOKDI69VUvdtlbSU9ugsnH8V/F9lxRtfVM7JSxVgrM1aVIPVl+Cv6OlEOG+j1BBQFSq6gyp7n1NtnoskxrrWpPW9rWshJ7fMSLOcLk2swRu6sa5Q0bNdtHBNUoDufG5B9LkJ/45t57GX23Hgnyh21Sq/Uj0/7TSH2ySkCl7ROZNeiameYhV6QY1uOqey9ic7j7Aq8WxI4Umbs+69D3EZ9+kFSz7mB0UV/KG7NkevmFR7qyjozblNjX/HEBQeMu8iuiY9pt+67qre0AOqTCAru1pf9OQwo+003nJ3zTkAEfUBJa/oruIXBrVHy7/bqG7gdu06wq7CVFsBV6mxihSNl546yd13S7I4W863pJmiJPfzel30k5vz97zOxjpFK8PvvA7fkmEODr0YEz5K7t7KLwypvnALvn+pmHDhg0bNmzYsGHDhg0bdw//B2ZHIJ6Dm6T8AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE4LTA5LTI2VDIzOjU4OjI4KzAyOjAwfzPYdQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxOC0wOS0yNlQyMzo1ODoyOCswMjowMA5uYMkAAABXelRYdFJhdyBwcm9maWxlIHR5cGUgaXB0YwAAeJzj8gwIcVYoKMpPy8xJ5VIAAyMLLmMLEyMTS5MUAxMgRIA0w2QDI7NUIMvY1MjEzMQcxAfLgEigSi4A6hcRdPJCNZUAAAAASUVORK5CYII=" + }, + "516d3969-5a57-5651-5958-4e7a49434167": { + "name": "SmartDisplayer BobeePass FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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MIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhOEV86zZ/8PMp0hD/Ud//AAAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,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MIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhMEA4jwBMEAIjxBMIAITxAMIMITBAOI8ATBACI8QTCACE8QDCDCEwQDiPAEwQAiPEEwgAhPEAwgwhOEV86zZ/8PMp0hD/Ud//AAAAAASUVORK5CYII=" + }, + "2c0df832-92de-4be1-8412-88a8f074df4a": { + "name": "Feitian FIDO Smart Card", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC" + }, + "c5703116-972b-4851-a3e7-ae1259843399": { + "name": "NEOWAVE Badgeo FIDO2", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAACqUlEQVRIx2P8//8/Ay0BEwONwagFpFlw8cKFirIyR3t7S1Oz0KDgBfPm//z5k3izvn39lp+Ta2tltWTRIoTofxhYtXKllpq6srwCAikoRIVHvH379j9x4NSpU0AtQI1W5hZwQagPzp87V11ZiXAvIxj9Zzh54kRNZRWRPvj96xcDOM0zMTKiB9G8uXP//fsHNFRASLC+sXHm7Nlubu4Qm3bt3Llu7VpiLGCEmcuIacGZU6fB4cWQX1AQGx/n7OIyaeoUbV0diIvamluePXtGUST/+g32HSODhoYGRISFhaWppYWVlRUo+OHjh6b6BoosgHvqz58/cDl9ff3M7CwIe8+e3atXrqQgmeIokDKzs/X19EGy/xk6OzofP3pEWUbDsAYYRC3tbRwcHED2h/fv62pqCReOjCTmZE0trZy8XAj78KFDy5YuJd50VAsYcepKTU83NjWBqOnu7Hxw/wE+O/7jsgC315mZmRubm9nZ2YFqvnz+0lBfhzOg/qO7lQm/B+EAmHwLioogCo4cOrxk0WIiPUEgkpFBUnKymZk5hN3T1XX3zh1iYoKJcDTBA4qFubmtlYubC8j++vVrTVU1qHQhzQeMBHyhrKxcWFwMUXn61Kn5c+dSv8JJSEy0trGGsCf099+6dQsuxcLCCrH7P5IrSYgDeKFS39TEx8sHZH//9r2uGhFQN65fh2VPNoqqTCUlpeKyUmgxfPpMSWERMAMuX7asv7cXIqilrYXwFrxeg/qOuGZSdEzM3t17Dh06CPT0pk0bN23cCI9FYKZJz8hE98Hff38hDDY2diL90dHdpaurixawrCysre3tunq6iLTX0NAAToIsTx4/tndwiIyOAtYExFjAzc3t4+sLJL99/QosE0VFRe3s7RtbmoGVFUqcjTYdh78FAIhBLlNd7ju1AAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAACqUlEQVRIx2P8//8/Ay0BEwONwagFpFlw8cKFirIyR3t7S1Oz0KDgBfPm//z5k3izvn39lp+Ta2tltWTRIoTofxhYtXKllpq6srwCAikoRIVHvH379j9x4NSpU0AtQI1W5hZwQagPzp87V11ZiXAvIxj9Zzh54kRNZRWRPvj96xcDOM0zMTKiB9G8uXP//fsHNFRASLC+sXHm7Nlubu4Qm3bt3Llu7VpiLGCEmcuIacGZU6fB4cWQX1AQGx/n7OIyaeoUbV0diIvamluePXtGUST/+g32HSODhoYGRISFhaWppYWVlRUo+OHjh6b6BoosgHvqz58/cDl9ff3M7CwIe8+e3atXrqQgmeIokDKzs/X19EGy/xk6OzofP3pEWUbDsAYYRC3tbRwcHED2h/fv62pqCReOjCTmZE0trZy8XAj78KFDy5YuJd50VAsYcepKTU83NjWBqOnu7Hxw/wE+O/7jsgC315mZmRubm9nZ2YFqvnz+0lBfhzOg/qO7lQm/B+EAmHwLioogCo4cOrxk0WIiPUEgkpFBUnKymZk5hN3T1XX3zh1iYoKJcDTBA4qFubmtlYubC8j++vVrTVU1qHQhzQeMBHyhrKxcWFwMUXn61Kn5c+dSv8JJSEy0trGGsCf099+6dQsuxcLCCrH7P5IrSYgDeKFS39TEx8sHZH//9r2uGhFQN65fh2VPNoqqTCUlpeKyUmgxfPpMSWERMAMuX7asv7cXIqilrYXwFrxeg/qOuGZSdEzM3t17Dh06CPT0pk0bN23cCI9FYKZJz8hE98Hff38hDDY2diL90dHdpaurixawrCysre3tunq6iLTX0NAAToIsTx4/tndwiIyOAtYExFjAzc3t4+sLJL99/QosE0VFRe3s7RtbmoGVFUqcjTYdh78FAIhBLlNd7ju1AAAAAElFTkSuQmCC" + }, + "c80dbd9a-533f-4a17-b941-1a2f1c7cedff": { + "name": "HID Crescendo C3000", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "820d89ed-d65a-409e-85cb-f73f0578f82a": { + "name": "IDmelon iOS Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAM1BMVEUtmc3y+fyWzOZis9rK5fI6n9B8v+Cw2ezl8vlHptNVrNbX7Paj0ulvud293++JxuP///89HRvpAAAAEXRSTlP/////////////////////ACWtmWIAAABsSURBVHgBxdPBCoAwDIPh/yDise//tIIQCZo6RNGdtuWDstFSg/UOgMiADQBJ6J4iCwS4BgzBuEQHCoFa+mdM+qijsDMVhBfdoRFaAL4nAe6AeghODYPnsaNyLuAqg5AHwO9AYu5BmqEPhncFmecvM5KKQHMAAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAM1BMVEUtmc3y+fyWzOZis9rK5fI6n9B8v+Cw2ezl8vlHptNVrNbX7Paj0ulvud293++JxuP///89HRvpAAAAEXRSTlP/////////////////////ACWtmWIAAABsSURBVHgBxdPBCoAwDIPh/yDise//tIIQCZo6RNGdtuWDstFSg/UOgMiADQBJ6J4iCwS4BgzBuEQHCoFa+mdM+qijsDMVhBfdoRFaAL4nAe6AeghODYPnsaNyLuAqg5AHwO9AYu5BmqEPhncFmecvM5KKQHMAAAAASUVORK5CYII=" + }, + "b6ede29c-3772-412c-8a78-539c1f4c62d2": { + "name": "Feitian BioPass FIDO2 Plus Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC" + }, + "85203421-48f9-4355-9bc8-8a53846e5083": { + "name": "YubiKey 5 FIPS Series with Lightning", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "d821a7d4-e97c-4cb6-bd82-4237731fd4be": { + "name": "Hyper FIDO Bio Security Key", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "9876631b-d4a0-427f-5773-0ec71c9e0279": { + "name": "Somu Secp256R1 FIDO2 CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "f4c63eff-d26c-4248-801c-3736c7eaa93a": { + "name": "FIDO KeyPass S3", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "d384db22-4d50-ebde-2eac-5765cf1e2a44": { + "name": "Excelsecu eSecu FIDO2 Fingerprint Security Key", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3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" + }, + "b93fd961-f2e6-462f-b122-82002247de78": { + "name": "Android Authenticator with SafetyNet Attestation", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAB7klEQVR4AaWPP2sUQRiHn5mdvexd/plEcvlDCi1E/EMabUWI9jaKWPoV/A7BQhAbG7t8CCUIKQQLuwhCUBsLBSUmGkLudm9n5nWHzMAego3P8Oy9s8vvfd+jzctPz2Ya+Zdbu48mG0ma8Eh8/bF3yWGGwPvV81d7+9/2lpy3Mrty7jswPPz8Yb20lQJ2iain2w9ok02aLURWstxuiHgknnrEK3GERg9poZ7s3CUxl/dvVfrntmRag9BuICJgrXfHnRvAWyJaDxXB+ezCWqX3t6e6i/ri/E1AkdBoLi/cZrL5pqeHb2yvu9RIUKfiWH95IVmmV6eucK1/j8JMIwRo6jNcX77P2vQ6ZEZ7OXreSFA93rnD3Mx6r7YfTxQKGkN4WP8eW7+bz4Z3eHEE9FFZAJXuliXVyUEfif9ZHINW+BQ5fSc+3oTjztTZRkx4LEhtfh1avBMSIkBrA+JvOAohm1AFgJGRpbOoXS/X1KXgHZE4X1Ssxpt18iYImGJiRFWWKCXkBdiR4L0QUEKamIKxhoQZm6fAdMDVjT7cQwBEYh3DSsl4A+trQTwJbUCsT5P+CodTZtYDmNJYcrEDQSChIMsVzoVQ2kLFMCCQFW4AoDbfbRDI7fIi5aAL41jtVNiQiPUjmUBOgAMCm683/ss/TaVXtx4qKMoAAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAB7klEQVR4AaWPP2sUQRiHn5mdvexd/plEcvlDCi1E/EMabUWI9jaKWPoV/A7BQhAbG7t8CCUIKQQLuwhCUBsLBSUmGkLudm9n5nWHzMAego3P8Oy9s8vvfd+jzctPz2Ya+Zdbu48mG0ma8Eh8/bF3yWGGwPvV81d7+9/2lpy3Mrty7jswPPz8Yb20lQJ2iain2w9ok02aLURWstxuiHgknnrEK3GERg9poZ7s3CUxl/dvVfrntmRag9BuICJgrXfHnRvAWyJaDxXB+ezCWqX3t6e6i/ri/E1AkdBoLi/cZrL5pqeHb2yvu9RIUKfiWH95IVmmV6eucK1/j8JMIwRo6jNcX77P2vQ6ZEZ7OXreSFA93rnD3Mx6r7YfTxQKGkN4WP8eW7+bz4Z3eHEE9FFZAJXuliXVyUEfif9ZHINW+BQ5fSc+3oTjztTZRkx4LEhtfh1avBMSIkBrA+JvOAohm1AFgJGRpbOoXS/X1KXgHZE4X1Ssxpt18iYImGJiRFWWKCXkBdiR4L0QUEKamIKxhoQZm6fAdMDVjT7cQwBEYh3DSsl4A+trQTwJbUCsT5P+CodTZtYDmNJYcrEDQSChIMsVzoVQ2kLFMCCQFW4AoDbfbRDI7fIi5aAL41jtVNiQiPUjmUBOgAMCm683/ss/TaVXtx4qKMoAAAAASUVORK5CYII=" + }, + "2fc0579f-8113-47ea-b116-bb5a8db9202a": { + "name": "YubiKey 5 Series with NFC", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "d8522d9f-575b-4866-88a9-ba99fa02f35b": { + "name": "YubiKey Bio Series", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "50a45b0c-80e7-f944-bf29-f552bfa2e048": { + "name": "ACS FIDO Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "f7c558a0-f465-11e8-b568-0800200c9a66": { + "name": "KONAI Secp256R1 FIDO2 Conformance Testing CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "3f59672f-20aa-4afe-b6f4-7e5e916b6d98": { + "name": "Arculus FIDO 2.1 Key Card [P71]", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAPoCAYAAABNo9TkAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAD6KADAAQAAAABAAAD6AAAAADrEeKkAAAACXBIWXMAAAsTAAALEwEAmpwYAAACzGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4zMDAwPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT4xPC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zMDAwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cl9EK38AAEAASURBVHgB7N1/jGVZQh/2e+6r7pnp39VdPT1dVd0zuwwLw9iE0PxY2yRuSIRDLLBj5MgEQgw4/iGwHAKJI5wfsmXFimUlVmJHSpRETkikSLEi5a9EimNGOJEcdoddkNdr0AJDdjzs7A4sC7sz01317sk5577qqf5dVe/X/fF5UF2v3rv33HM+p7aqvnPOPaeqPAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwIoEwoqu4zIECBAgQIBAvwXy3wz1rAkxfW763Ry1J0CAAAECBAgQIECAAAEC/RPwH/T712dqTIAAAQI9FPALt4edpsoECBAgQGCFAnnUvHn+xo2vmjbNX6pCeCb98fDL77z55l9eYR1cigABAgQIjEJgYxSt1EgCBAgQIEDgpAIloO+H6YfryeSHQghV08RPpcIE9JOKOo8AAQIECDxGQEB/DIyXCRAgQIAAgQ8E6jjZirGp8s3nIdS//cE7nhEgQIAAAQKLEjhY7GVR5SmHAAECBAgQGKBAUzXX8+h5SuepddF/4B9gH2sSAQIECKxfQEBffx+oAQECBAgQ6LxAHcLFNpx3vqoqSIAAAQIEeisgoPe261ScAAECBAisTiDNbr9YxTzB3YMAAQIECBBYloCAvixZ5RIgQIAAgWEIlP3OQ4jXhtEcrSBAgAABAt0VENC72zdqRoAAAQIEuiBQhs3T4PkLXaiMOhAgQIAAgSELCOhD7l1tI0CAAAEC8wvkgJ5uQQ/nywru85enBAIECBAgQOAxAgL6Y2C8TIAAAQIECFR5yfZqd3f3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gAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCLRX4H8D7duTS/D4+v0AAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAPoCAYAAABNo9TkAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAD6KADAAQAAAABAAAD6AAAAADrEeKkAAAACXBIWXMAAAsTAAALEwEAmpwYAAACzGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4zMDAwPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT4xPC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zMDAwPC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cl9EK38AAEAASURBVHgB7N1/jGVZQh/2e+6r7pnp39VdPT1dVd0zuwwLw9iE0PxY2yRuSIRDLLBj5MgEQgw4/iGwHAKJI5wfsmXFimUlVmJHSpRETkikSLEi5a9EimNGOJEcdoddkNdr0AJDdjzs7A4sC7sz01317sk5577qqf5dVe/X/fF5UF2v3rv33HM+p7aqvnPOPaeqPAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwIoEwoqu4zIECBAgQIBAvwXy3wz1rAkxfW763Ry1J0CAAAECBAgQIECAAAEC/RPwH/T712dqTIAAAQI9FPALt4edpsoECBAgQGCFAnnUvHn+xo2vmjbNX6pCeCb98fDL77z55l9eYR1cigABAgQIjEJgYxSt1EgCBAgQIEDgpAIloO+H6YfryeSHQghV08RPpcIE9JOKOo8AAQIECDxGQEB/DIyXCRAgQIAAgQ8E6jjZirGp8s3nIdS//cE7nhEgQIAAAQKLEjhY7GVR5SmHAAECBAgQGKBAUzXX8+h5SuepddF/4B9gH2sSAQIECKxfQEBffx+oAQECBAgQ6LxAHcLFNpx3vqoqSIAAAQIEeisgoPe261ScAAECBAisTiDNbr9YxTzB3YMAAQIECBBYloCAvixZ5RIgQIAAgWEIlP3OQ4jXhtEcrSBAgAABAt0VENC72zdqRoAAAQIEuiBQhs3T4PkLXaiMOhAgQIAAgSELCOhD7l1tI0CAAAEC8wvkgJ5uQQ/nywru85enBAIECBAgQOAxAgL6Y2C8TIAAAQIECFR5yfZqd3f3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gAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCLRX4H8D7duTS/D4+v0AAAAASUVORK5CYII=" + }, + "42b4fb4a-2866-43b2-9bf7-6c6669c2e5d3": { + "name": "Google Titan Security Key v2", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAD1ElEQVR4AeyXU5gcTRSG62L927b/aJLpnt241zGmd2Pbxl1s29Yotm3bdqY3nN5oUakTq43leZ5v2Xi/qoMalBkiO84xTND1qNAwbyTdkshBtM4bZTtFvp97Tdu4SHo6UVu4Fu5Jc3AA4aKo0QTuNhFWKI5oPDzDdHACzcCKAog+gh2zFjIc/CYT+iN52TIibIQgxW4zlk8NSheq7PNtxwbrku5p5Y2gu8PDTdQDLspWUh/4KHqwqfCgCPqYl6G/NXLl0z/8jSiK1Qhz+7UZwJkKnxBj/dcbafMpBE56PsQqvq+TwN+eL4oDrjUMHoKLpFcphJ+s5OXXmLBfyQJ5DIGHdqlkml6PpiORyoCjB4E/pBs8Xof8+EHfnuCKWVNkwF+DVNP8TobxQ3pF8qoANmm1P37o/AgnxOcRgbf5rsdQOVF6i6TVAcvAAOjx0kB8p/HfQiYqpjd2SJ8vCXgSwL8uvucP2BtNvQ6/DKXHSF4dUBGA36cHkz7DCWUtAI+5aBuVPg2s8h8OsEJ6ND8EUuooSq9BILcBqLj82iKFEdGTx3oqvAe/TKiAr0kZeLzSn0pjA6BzQjuApUQK/cN0YCBJtQEEkfYGcJY1ACkUlJ4NcDKK2JKeDeySNLDav2E6MHBJYBL7jxeD51cFJfeel2+pCgOT5Qp6vOQc6MWvEjJQUwj+9IrPcAVPDKacLLbOYv9FBkVkT76t5A70ShwucJgL/vF98CuW/IyLuMoC/DM52OnGGfBtkzIQ2cNXUew4sekF+MPVAbjfgrwA/Y5oR1yk3vDhPRLLyqkBph//LYIQS6PrKz/CdWczACuka6Ez7D/qBc908X5I4I5J5n/PxE1SnwmCNi79/kasuyRASukY7Y7X5bNsRE/fPDmrT1KsKZIKymGvC4AydYpyls+paeV78Itkts/bTBcsb5ASsF0KTDygXPbOzORaiqZ0PhcbGzax65HwPl6Z/T+xs/yHO+1hBCwJAOXLztFOtjfcK/hcd/yflINtSq7b9uI+2/TGmBlwVPIILbD6wgEvgheo1G2ifUTrQAAMBoWup2dVwYWGLRcpXj4WqQmrk50MLzBLBcaOJIPq7psGevi6q29v6xg/yhXnMdOEbWo7HN733Iu895DU8QMWTSZg+pppgp5V7XHRwVtfwOsTJI9bQscxx0TcYFg4pHeQwWUhLzhkGDgUuotlkZEBK2N1sTVJgZ/TEf4BeWZ/y7xynyKzAgYX7bBXJEW+THpmCOqU1RHX0Tqr8pcoLQMOdmAGchd6/vPdSXonPWDCPxmwQADibHC/YhiAUQAA0S0KWSVGA04AAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAD1ElEQVR4AeyXU5gcTRSG62L927b/aJLpnt241zGmd2Pbxl1s29Yotm3bdqY3nN5oUakTq43leZ5v2Xi/qoMalBkiO84xTND1qNAwbyTdkshBtM4bZTtFvp97Tdu4SHo6UVu4Fu5Jc3AA4aKo0QTuNhFWKI5oPDzDdHACzcCKAog+gh2zFjIc/CYT+iN52TIibIQgxW4zlk8NSheq7PNtxwbrku5p5Y2gu8PDTdQDLspWUh/4KHqwqfCgCPqYl6G/NXLl0z/8jSiK1Qhz+7UZwJkKnxBj/dcbafMpBE56PsQqvq+TwN+eL4oDrjUMHoKLpFcphJ+s5OXXmLBfyQJ5DIGHdqlkml6PpiORyoCjB4E/pBs8Xof8+EHfnuCKWVNkwF+DVNP8TobxQ3pF8qoANmm1P37o/AgnxOcRgbf5rsdQOVF6i6TVAcvAAOjx0kB8p/HfQiYqpjd2SJ8vCXgSwL8uvucP2BtNvQ6/DKXHSF4dUBGA36cHkz7DCWUtAI+5aBuVPg2s8h8OsEJ6ND8EUuooSq9BILcBqLj82iKFEdGTx3oqvAe/TKiAr0kZeLzSn0pjA6BzQjuApUQK/cN0YCBJtQEEkfYGcJY1ACkUlJ4NcDKK2JKeDeySNLDav2E6MHBJYBL7jxeD51cFJfeel2+pCgOT5Qp6vOQc6MWvEjJQUwj+9IrPcAVPDKacLLbOYv9FBkVkT76t5A70ShwucJgL/vF98CuW/IyLuMoC/DM52OnGGfBtkzIQ2cNXUew4sekF+MPVAbjfgrwA/Y5oR1yk3vDhPRLLyqkBph//LYIQS6PrKz/CdWczACuka6Ez7D/qBc908X5I4I5J5n/PxE1SnwmCNi79/kasuyRASukY7Y7X5bNsRE/fPDmrT1KsKZIKymGvC4AydYpyls+paeV78Itkts/bTBcsb5ASsF0KTDygXPbOzORaiqZ0PhcbGzax65HwPl6Z/T+xs/yHO+1hBCwJAOXLztFOtjfcK/hcd/yflINtSq7b9uI+2/TGmBlwVPIILbD6wgEvgheo1G2ifUTrQAAMBoWup2dVwYWGLRcpXj4WqQmrk50MLzBLBcaOJIPq7psGevi6q29v6xg/yhXnMdOEbWo7HN733Iu895DU8QMWTSZg+pppgp5V7XHRwVtfwOsTJI9bQscxx0TcYFg4pHeQwWUhLzhkGDgUuotlkZEBK2N1sTVJgZ/TEf4BeWZ/y7xynyKzAgYX7bBXJEW+THpmCOqU1RHX0Tqr8pcoLQMOdmAGchd6/vPdSXonPWDCPxmwQADibHC/YhiAUQAA0S0KWSVGA04AAAAASUVORK5CYII=" + }, + "361a3082-0278-4583-a16f-72a527f973e4": { + "name": "eWBM eFA500 FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "692db549-7ae5-44d5-a1e5-dd20a493b723": { + "name": "HID Crescendo Key", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "bbf4b6a7-679d-f6fc-c4f2-8ac0ddf9015a": { + "name": "Excelsecu eSecu FIDO2 PRO Security Key", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3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" + }, + "3e22415d-7fdf-4ea4-8a0c-dd60c4249b9d": { + "name": "Feitian iePass FIDO Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC" + }, + "aeb6569c-f8fb-4950-ac60-24ca2bbe2e52": { + "name": "HID Crescendo C2300", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAVMAAACsCAYAAADG+E8MAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAAJcEhZcwAAD2AAAA9gAXp4RY0AAAygSURBVHhe7Z1/bJTlHcBvjhjNcC4O+dXeXVtUTMziP7oYXZY51IkKd1fNnFHj5ohBmA7j2MRsZolmxhhNJort24KgsiFsim7TAdMYRFQEFTcVxw/rwAEFRChQ+uuePc/1qQP3TNs+33veu+vnk3zS42gfnve9t58+773XIwEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUEpkG6/XPpnIRR8gIh5t41r9cYatBfwP9Q3n6x20TZtP1DcpRMTPNdeU14uuVt2Mq21FBkxtMjmrLpVq0R8311ZX32rvLmMKP230jqmP3DsNEfHzzEW7ExfOGWmL8oWkk8kf1qXSPXXVqaXJUaPOqKmqOrMumfprbTLVnUqlLrefVkZMmP11/ZOlw7lzEBEHojmrzUZTbV3+L3Vjx04wIR09evTJ41KpKdobjCNHjhw1duzY5Lh0jdKr1LPtp5cBJqSsRhFR0t6gzrSVcXGMDqmqSSYz+vYwE86aqtS1tdXp683tujFjUjVjk5P1KrW999PLgVzU5dwZiIg+mqBeOqfOluYo0un0cTqmXfaPw8wK1d5O6FP8t2rT6Vv0zS+bsPbeW+rkoo+cOwERUcJcdMDW5iiqq6uPH5eq6Vt1FlamOqI761I1209J1/RF9kvlEdP6hm87Nx4RUdJswz22Op9iYqpXo532j2Zlmj/ppJO+qj92p8eMOd3ef0x5xDTXtM+54YiIkuaiDludI+k9hU8njtO3CzE1d44YMWKMvn3Q3B4+evjJ+nbfKrWE4XWkiBjKy5vPsuX5lLpUamZtMr3f3K6tTr5TuFNTl0w+WpNK3az/rqO2Oj3N3l2iTI6mOjcYEbEY5pqetfU5irrq1DO1ydSBcVWpG+xdibqq5AyzOtX3L7R3lTD10XLnBiMiFsNcU+HU3UVyVPIMHdWVp9XWqVNravP69vKqEVWn2r8uceqj/c4NRkQshrmojF4vOhCIKSKG1H0RqgIgpogYUmKKiCggMUVEFJCYIiIKSEwREQUkpoiIAhJTREQBiSkiooDEFBFRQGKKiCggMUVEFJCYIiIKSEwREQUkpoiIAhJTQS97WCUueEAlLpwdVvNv5iL3nAbr9x50/1vF9iKtaz4DMa7HwDz+rvn0x6x+/OKYdzE023GRPn7MMXSp3ieTG93bXGkSUzlvnvuyiovjrpznnNOg1Af/us277Mhh2fnJod5vQNe8+qP+Jo6LadEq95z64deuXWBHqQw6u3tUW3un2rxjn1q9Yadasnqzuqn5ZXXyNQtU4uKHVCJTgYElpnKab6a4qJSYfrTnQNnG9IaHX3LPqR+eqCMzVNiz/7Ba8dZWdeV9z6vEBL2KrZSwElM5iak/xHRo0dnVo55d96Eaf+Miv6dJSkFiKicx9YeYDl3ebtmjzpu11O/xj1NiKicx9YeYwhtbdqlTpuqVqrko59hXJSsxlZOY+kNMwzPrsTXqzsVvqLuWvKEydy9TuXuWq18ufL1w371L16sV67cVLiaFpCefV4+++E+VuGC2c3+VpMRUTmLqDzENT2LCb/UqsFElMg3/nZO5KFS4TztJPx6XzlFVUxaqKXNWqo/bDtuvLD6729rVN366xITqqP1VkhJTOYmpP8Q0PIXXhjrm5FRH7ZjJDeqO36+1X118unt61C2PrNbH5RGxL0WJqZzE1B9iGp4BxbRPHbZJdy+zI4Rh/gvvF1bIzvmUgsRUTmLqDzENz6Biasw0qh/r0/6QPPnqB37HRzElpnISU3+IaXgGHVNjNlJ//3CPHSkMT7/WUppBJaZyElN/iGl4vGKqHf+TxXakcPzxFb1CLbXnUImpnMTUH2IaHt+Ymqi9t22vHS0cP1vwqns+cUlM5SSm/hDT8HjHNBep825/2o4Wjnw+r8ZPX+yeUxwSUzmJqT/ENDzeMdV+5apH7Ghh2XewQ2T+IhJTOYmpP8Q0PCIxmmRO9T+xI4blmTUthdWxc14hJaZyElN/iGl4RGKajdQt816xI4Zn+FWCx/9gJaZyElN/iGl4pE6Tz5yxxI4Ynvc/2tv766+OeQWTmMpJTP0hpuGRiuno6x+3I8bDiOsedc4rmMRUTmLqDzENj1RMh13RbEeMB3PMxvrcKTGVk5j6Q0zDIxVTcxGqq7vbjhqeru4euW0ZjMRUTmLqDzENj1iA9HGzdlOrHTUebp0f4wv5iamcxNQfYhoesZhmGtXClRvtqPGwbbc+fuJ6h35iKicx9YeYhkcspjpitz22xo4aD+0dXSoxMaa36SOmchJTf4hpeCRjGudrTfuI7ao+MZUzzph+51d/UufOelrEb/78KbUhhjeuMBDT8IjFNKbf0f8stz2+xj2/YktM5YwzppUCMQ2PWEy159y21I4aH6ve3e6cW9ElpnISU3+IaXgqLaZb47oIRUzlJKb+ENPwVFpMt+892Pu/qjrmV1SJqZzE1B9iGp5Ki+mufe0qlnfhJ6ZyElN/iGl4Ki2mhfc4vczjGBqsxFROYuoPMQ1PxZ3mf8xpvizEtCwhpuGptJju2HuImIpCTMsSYhqeSovpBzv3m7A551dUiamcccbUvMHE60Ku2bhTHWjvsiOHhZiGp9JiumT1Zufcii4xlTPOmB5rfhKbJ90lvPgh9frGeN79h5iGRyymJfIbUPX3LHfPr9gSUznjjCm/m28lpgNGLKYl8rv5sZziG4mpnMTUH2IaHsmYTo/5usH+Q529Z1eu+RVbYionMfWHmIZHLKaZRrXopU121HhY37Kblak4xHTwEtNBQUwb1Yr12+yo8XD2zKXuuYWQmMpJTP0hpuERi+nkBtX6ySE7anja2vUp/iUxvTG0kZjKSUz9IabhkXzONE6eWLXJPa9QElM5iak/xDQ8UjE98Zr5dsTw9PTk43nbvSMlpnISU3+IaXikYnrq9CfsiOH5y7p/mZg55xVMYionMfWHmIZHJKY6ZJfc+ZwdMSyHO7v1MRPjc6V9ElM5iak/xDQ8IjHNNKolq7fYEcMyrXGVe06hJaZyElN/iGl4RGIa08WnTdv3xfci/c9KTOUkpv4Q0/BIxHT8tEV2tHC0d+jTe32suuYTi8RUTmLqDzENj3dM9Sn+3Oc32NHCYK7enzXzSfd84pKYyklM/SGm4fGN6fAfzLMjhWPGvJedc4lVYionMfWHmIbHK6aTG9Tcv4Vdld6+cI0Jl3s+cUpM5SSm/hDT8Aw6ptlInX/Hn+0oYbipeVU8/yVJfySmchJTf4hpeAYV00yDOvf2Z+wIxae7J69+NPvF0lyR9klM5SSm/hDT8PQ7piZk+rTeHGv3PrXefnXxOdjeqcZNXeSeUylJTOUkpv4Q0/AkvnV/77stfdaJD6lhVzSrE6+er06/abHK3L1c/SHwC/OXvbm1MA/XPis5iamcxNQfYgqGg4c71VX3P19YCbv2V0lKTOUkpv4Q06FNR1e3enjZuyrx3Qec+6mkJaZyElN/iOnQpL2zSzWt2NB7Sl/KF5k+T2IqJzH1h5gOHfL5vHq7ZY+aMmelSlygV6LlGtE+iamcxNQfYlrZfNx2WK16b4e60bzTU7ZRJSZ5PNalJjGVc9Jvlqnlb24tXIEM6cp3/q2O/f5c55wGZaZRPfjsP5z/VrH93cqN+hvM46LDxDnqpXe3O8cupive2qYuues595z64QlXz1e797erlta2ivDNLbvV2k2thX3z6yfWqol3PqdOMD/wL9an8fqHtWsflL3EFLEENKe45uVIZlVe7prtMFfhy+lKvITEFBFRQGKKiCggMUVEFJCYIiIKSEwREQUkpoiIAhJTREQBiSkiooDEFBFRQGKKiCggMUVEFJCYIiIKSEwREQUkpoiIAhJTREQBKzamuajVucGIiMXxoK1PhZFtaHJsLCJiccxFu2x9Kowrmsc7NxgRsRhmol/Y+lQg5jkM10YjIkqai/K2OhVKrukF54YjIkqai3bY6lQwuajbufGIiBLmtOfcd7wtTgWTi6Y7dwAiooS5aJmtzRCgPnrNuRMQEX3MRq22MkOIbONG585ARByMuaYKfSlUf8hFi/QOyOuVqnvnICJ+kebKfX3TWluVIUw2Ok2vUluJKiIO2Fy0N5Ftus7WBAqYqNZH6/THfTqsnYn6Zr2zEBGP0KxCs1GbbsSWRKZhgq0HAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBpkUj8B4Aom+MbT+3JAAAAAElFTkSuQmCC" + }, + "87dbc5a1-4c94-4dc8-8a47-97d800fd1f3c": { + "name": "eWBM eFA320 FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "9f0d8150-baa5-4c00-9299-ad62c8bb4e87": { + "name": "GoTrust Idem Card FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "12ded745-4bed-47d4-abaa-e713f51d6393": { + "name": "Feitian AllinOne FIDO2 Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC" + }, + "88bbd2f0-342a-42e7-9729-dd158be5407a": { + "name": "Precision InnaIT Key FIDO 2 Level 2 certified", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "34f5766d-1536-4a24-9033-0e294e510fb0": { + "name": "YubiKey 5 Series CTAP2.1 Preview Expired ", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAfCAYAAACGVs+MAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAHYYAAB2GAV2iE4EAAAbNSURBVFhHpVd7TNV1FD/3d59weQSIgS9AQAXcFLAQZi9fpeVz1tY/WTZr5Wxpc7W5knLa5jI3Z85srS2nM2sjtWwZS7IUH4H4xCnEQx4DAZF74V7us885v9/lInBvVJ/B4Pv9nu/5nu/5nvM556fzA/Qv0Hb/IrX3VFKPo45cnm4inUIWYwLFRmZQUuwjFG/N1iRHh1EZ0NRVRudqt1Bd+2nSKyS/Ohys0+lk3e/3kQ9qvD4ZUta4VVSUuY0eipyiThAfocoORVgDuuw3qKRiAd3rbcEtjTjYIof6WaHsCmzVPWCMx+cgh8tLqWMKaMWsUjLqo2RtJIQ0oOzmerpQu4esZgsONkGxH7d0kdvTT17s4OMU7VI8ZhjgGaM+Aq9iENu8Pif1udz07MwvKWf8GlVoCEY04PC5WdTaXYFbR8vNvL5+3Kgfb5xNMya9RamJiynaMlGTVtFlr6ba9u+pqnEX4uMuRRgjSYEhrN7utFFe6lqal7Nfkw5imAGHynPpbk8VmY0xstnptlFCVCYtzTuBN83QpMLjTtevdPzSUnJ7e8mkjxZ39fXbKDfldZqbvU+TUgGnBVF6fQ2iPHg4W16UWUwvzbk16sMZE+Pn0pvz7JSeuAyes8lcpCmaKuo/p+qWr2UcwIAHWrvP0YEzhXAtLAbssHhp7iGamvyijP8ryqrXUWX9XoowxyAufNBrp43POBFXZlkf8MDRiqcpyowAwpuz2x+fWvz/Dtde9smszygtcR6C1wbdzBl6Olq5WNYY4oGathJMrkTEx0jARSHAVs+5rYkQNXb+QgfPLsQ6gXyInsreQfmpm7RVFYfL86n1fiUOkYvShkUPxvbukzoy6K1ihM1ho3XzW6EvSfXA+dpiWGaWd+doXzLzmGwKYFLCAsRAlPBAhMlCFXU7tBUVPr8HgVcJHWq+F00plr+DMTdrP4zvxY11kNMhxT+SeTGg+d4V5LQJityUGJNB8VFZsjgYBZM/II/XCTkj0qyDOpF2AVQ17CIjUp/DnT1UkL5F5gdj+sS1wg1gE3gigm60fCXzSnPXbyAPbIXv+IDpE16ThaHIS9skyhlmME5F3cfqAKhq2C0E5PH1gYaXaLPDkZG0HDJOnKWHp51I0z5SOux8e1WAuZzdHQrTkp8TmjXoI+la0wGZszubqbO3ifQ6A/W7vVSYsV3mR0JKwkKc4WHiBkmR8I3CCgI87oOL4qzT5P+RUJBejEOgAPK8hYPzatM+eITp2IO9yTQmeromPRxx1qxAcsile/ubSeEbcWQGYECghcLY2HyKjogjH25hMpjpUv1Ougli4eh2eRw0O32bJjkyuCgNzg0vzlYMSiSs0uoo4MG7hMOjCEaX1yFE0nSvjBzuTnEpK86Z8IoqFAIubw8kg9ArEaREWSZI+jH4Xbp6g9E9EnJT3oaRzDN+MUJBQDHn56a8oUmEBusOxBs/N5+tJEbPkAFDj8UGvOs/IWvcSglGBhvS7/FTYfpWGYdDY8fPAxWSA35sTC4p4+Lm4AaqIoPeQtfufK6Jh0ZhxlbsUXOSmXNifD5ZTAkyDofbbcclxnA8WNAqxCbRNykhXxQpaDw67fXUYbsiG0Khtv2oeIvh8rhQMYOcEAqXG/eI+zngOc5yxr8q82IAM1c/FLFOplqu5eFQXrMZzGcVCjYbLWG5I4BT1euRrlbxtNOtMitDDEhLXIIynAAvuOEWE3X3NdAft94VgaG42XIQt0ZX6PeCE/qQFe9rK6Hx7YU50KvH7fW4fS+q7KKBJxsggBX5pSAGh1jIrVh5zQ6w3RfaahBXm/aCbCZTjCUFUTyWZqW9p62MjJPXVqOrPgMO4Nv74Gkf+owftNVBDQnjFJqHSw17pXvhWW5KZqe/Q49N/USTCAVWoQXFIHBHXXe3FPrUDsuGDmtF/hHKTHpekxhiAOPI+SJq6S6HF4I9YWzkBJTo46iUMzWp8Pir/RiduLxKYsSksV8vLlOQvhGX2YlR0OBhBjC+u/gEcvY0ApK7Yk41NxjPSQnWFHTF66UrjgevB8Cu5a+l2vYSRPtuVDo73hhdMSHnUX7tTjsVZGxAl/WptiOIEQ1gnL29mX6/tR1tmlkYj8W4X+CSjWcUDGY1NpS/C7hSKqiMLM/l2QmSWZ73Ddz+gio8BCENYPQ46qnkzwXUbqvBkxjUQsWfZFgbuo3rAf+wN7jOO90+ynx4Pi3L+0nYL1SchDUgAP4gPV/7Id1q+1HShmuGkIqWRPgyxMFqP8HfjTnjXwY5bQfbJct6OIzKgMHotF/He1egsaxHSqG6wfdmQ5x8NyTFFqBcp2iSowHR3yk5+36hF7vXAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,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" + }, + "83c47309-aabb-4108-8470-8be838b573cb": { + "name": "YubiKey Bio Series (Enterprise Profile)", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "be727034-574a-f799-5c76-0929e0430973": { + "name": "Crayonic KeyVault K1 (USB-NFC-BLE FIDO2 Authenticator)", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "58b44d0b-0a7c-f33a-fd48-f7153c871352": { + "name": "Ledger Nano S Plus FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASYAAAEACAYAAAAeMdvxAAAAAXNSR0IArs4c6QAAAIRlWElmTU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAAEsAAAAAQAAASwAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAASagAwAEAAAAAQAAAQAAAAAAe6SCkwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDYuMC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KGV7hBwAAD65JREFUeAHt3LuOJGcVB/Bd9mIHNhLiIhOQOEaCCDkiICNG4g38CjwJCQlCBASIBN6ChAgJJERiJAvZAoyxfFnvhe/s9JFqe3tmuk9/p6d651fSN1VdVedUza9q/l299sydO3fuvD/GszGebOaxbKzX4NHm+vxqzGN6cDHzdSFwf7P88zGPeznN3Nfrva/j2jzdXK9PvzIWTAQIEFiVgGBa1eVwMgQIhIBgch8QILA6AcG0ukvihAgQEEzuAQIEVicgmFZ3SZwQAQKCyT1AgMDqBATT6i6JEyJAQDC5BwgQWJ2AYFrdJXFCBAgIJvcAAQKrExBMq7skTogAAcHkHrgtAvFLoqYzERBMZ3KhFqd5d7Oc88Umi5cIhBWvS3DWuDr/PMQx5+ad6Bi9w2vTO+eHd7g9FWmUf07j9nznN/+dHvVGEMXx95i+PUZcvH2foPKCR/1Px/jjGG+OEX/T6agTGvWmqwXC/t4Y/xkjrl145/UYi6YhkCZvjeVvjPF4s27MTE0CcQ/Gg87HY3x/jN+PEVOs3zcTct/PZjwx/WUc+L04A9PJBfIH8OQHXvkB8wb/5zjPGKbTCjw89nAzgumNzUnEycQTk6lfIAIpnnBjmHYLRDjFJ4AYsWzqF4i/pvr5GJkJ5SPOCKYMo5jncvmEFBKYKCC8J2Lu0So/ssVH56Omff9N6aiDKCZA4FYJZECVv2nBVKZTSIBAl4Bg6pLVlwCBsoBgKtMpJECgS0AwdcnqS4BAWUAwlekUEiDQJSCYumT1JUCgLCCYynQKCRDoEhBMXbL6EiBQFhBMZTqFBAh0CQimLll9CRAoCwimMp1CAgS6BARTl6y+BAiUBQRTmU4hAQJdAoKpS1ZfAgTKAoKpTKeQAIEuAcHUJasvAQJlAcFUplNIgECXgGDqktWXAIGygGAq0ykkQKBLQDB1yepLgEBZQDCV6RQSINAlIJi6ZPUlQKAsIJjKdAoJEOgSEExdsvoSIFAWEExlOoUECHQJCKYuWX0JECgLCKYynUICBLoEBFOXrL4ECJQFBFOZTiEBAl0CgqlLVl8CBMoCgqlMp5AAgS4BwdQlqy8BAmUBwVSmU0iAQJeAYOqS1ZcAgbKAYCrTKSRAoEtAMHXJ6kuAQFlAMJXpFBIg0CUgmLpk9SVAoCwgmMp0CgkQ6BIQTF2y+hIgUBYQTGU6hQQIdAkIpi5ZfQkQKAsIpjKdQgIEugQEU5esvgQIlAUEU5lOIQECXQKCqUtWXwIEygKCqUynkACBLgHB1CWrLwECZQHBVKZTSIBAl8D90fjLTfNHY35vjGeb13d3LC/XxW4PF/vEa9PpBOJaPBgjr9chR87rmNf+kFr7ErhOIO7JvLfy/sx7LmqXy8vXse/zTIov34wtY3r9Ynbw1/jhMJ1WIC9svJmYCKxFIO7LmCJXjsmFr0aDX48R4RQ3+b4f7TIF4+AfjBFTrrt45WuXQIbSt8YBfjzG48WBclusyptkeV1ye1z3/47xhzGejmEiMEMg76V/j2a/3TSM+y/vxeuOEftGBn1x3Y77bt/3wPv2s9/lAvFxO6YfjREXsjo+HLXxUTwm1+/CwdfjBabcS/HOGQl1TLNIyfjhMJ1WIJ+U4rN8XL99r2Fcr3jS/WgM120gmKYK5D2Vb6CV5s8imPIdt9IgavJEqvXqjhOIG2DfUFrut+/H9uPOTvVtFciPdaXvP4OpVKxoVQLL0LnqxHK/nF+1r20EqgJHPbB416yyqyNAoE1AMLXRakyAQFVAMFXl1BEg0CYgmNpoNSZAoCogmKpy6ggQaBMQTG20GhMgUBUQTFU5dQQItAkIpjZajQkQqAoIpqqcOgIE2gQEUxutxgQIVAUEU1VOHQECbQKCqY1WYwIEqgKCqSqnjgCBNgHB1EarMQECVQHBVJVTR4BAm4BgaqPVmACBqoBgqsqpI0CgTUAwtdFqTIBAVUAwVeXUESDQJiCY2mg1JkCgKiCYqnLqCBBoExBMbbQaEyBQFRBMVTl1BAi0CQimNlqNCRCoCgimqpw6AgTaBARTG63GBAhUBQRTVU4dAQJtAoKpjVZjAgSqAoKpKqeOAIE2AcHURqsxAQJVAcFUlVNHgECbgGBqo9WYAIGqgGCqyqkjQKBNQDC10WpMgEBVQDBV5dQRINAmIJjaaDUmQKAqIJiqcuoIEGgTEExttBoTIFAVEExVOXUECLQJCKY2Wo0JEKgKCKaqnDoCBNoEBFMbrcYECFQFBFNVTh0BAm0CgqmNVmMCBKoCgqkqp44AgTYBwdRGqzEBAlUBwVSVU0eAQJuAYGqj1ZgAgaqAYKrKqSNAoE1AMLXRakyAQFVAMFXl1BEg0CYgmNpoNSZAoCogmKpy6ggQaBMQTG20GhMgUBUQTFU5dQQItAkIpjZajQkQqAoIpqqcOgIE2gQEUxutxgQIVAUEU1VOHQECbQKCqY1WYwIEqgKCqSqnjgCBNgHB1EarMQECVQHBVJVTR4BAm4BgaqPVmACBqoBgqsqpI0CgTUAwtdFqTIBAVUAwVeXUESDQJiCY2mg1JkCgKiCYqnLqCBBoExBMbbQaEyBQFRBMVTl1BAi0CQimNlqNCRCoCgimqpw6AgTaBARTG63GBAhUBQRTVU4dAQJtAoKpjVZjAgSqAoKpKqeOAIE2AcHURqsxAQJVAcFUlVNHgECbgGBqo9WYAIGqgGCqyqkjQKBNQDC10WpMgEBVQDBV5dQRINAmIJjaaDUmQKAqIJiqcuoIEGgTEExttBoTIFAVEExVOXUECLQJCKY2Wo0JEKgKCKaqnDoCBNoE7rd11vgcBOL6Pxnj3hjPzuGEDzzHp2P/GKYzExBMZ3bBJpxuBlAE0mebfq/yD+/d8T3m9zyBT4tTCAimUyiv6xjxgxrTm2P8ZIwvx4iP9K/SD298L6+N8acx/j6GcBoIJgKdAvGxK6YfjhE/gPHkE088sbzvOHT/ffuubb+fDZOYHlzMfD0XAU9M53Kl5p5nPjVlQOXrCJaYdr2Obcsnj1zOfZ8X7viy7Jk9crfcFq+XfXK/3L7clrU5X+6Ty4/Hxnhi+iJ3Mj8vAcF0Xtdr9tnGD/zyh365HMdavs7lnG9vj9e7pqv2X25b1ub6nC+3bS8v98nl/K/N+Xq7xuuVCwimlV+g5tN7VX9wX9Xvq/l2WE/7fGdZzxk5EwLHCeTHueO6qL5RAcF0o/wO3iDgaakB9dQtBdOpxR2vW8ATU7fwCfoLphMgO8RJBTwxnZS752CCqcdVVwIEjhAQTEfgKV2lgI9yq7wsh52UYDrMy97rF/BRbv3X6NozjP+P6dgL6R3qWubWHfi/yBseTF40uYlXR+WKJ6abuGQ9x8wfxpznUS77Qd3eL/eP+XLbcjm35brL5tkrtx/6elkXy8vX2Svny+25X85zH/MzE4gnJhfxzC7a5nTzl3lznt/F9jvV9uvL9sv1MV/WLJcv25b75Dx7VV8v65bL2Xc5X27P5YebHfzy7lLqtMtH5UpcyN+N8dYYj8aIJ6hDGkawvTvGXze18Uuhpl6BuGZxjb42xg/GiL8uEFP+UF68ut1f4z6MX+L98xjvjZFmY9HUKBBvknE/vj3GLzfHOSRPYt/o8XnUfjxGrKiOd6LJmLbfuS/W+tohIIT2V2W1v9Wxe+YT6vdGo2qePK+LJ56Pxog/GpZPTGPx2imKY4oTiT8xYTqtQPjHD5w3g6vd48nJU/zVRjO3Zi7EU1M+yee6fY4T+0YmfRJfYsQU833/MXx5MO9Iz/lO/iWugTeFk7M74B4CyzfNuE/3zYjc9/6+QbTHudiFAAECcwQE0xxHXQgQmCggmCZiakWAwBwBwTTHURcCBCYKCKaJmFoRIDBHQDDNcdSFAIGJAoJpIqZWBAjMERBMcxx1IUBgooBgmoipFQECcwQE0xxHXQgQmCggmCZiakWAwBwBwTTHURcCBCYKCKaJmFoRIDBHQDDNcdSFAIGJAoJpIqZWBAjMERBMcxx1IUBgooBgmoipFQECcwQE0xxHXQgQmCggmCZiakWAwBwBwTTHURcCBCYKCKaJmFoRIDBHQDDNcdSFAIGJAoJpIqZWBAjMERBMcxx1IUBgooBgmoipFQECcwQE0xxHXQgQmCggmCZiakWAwBwBwTTHURcCBCYKCKaJmFoRIDBHQDDNcdSFAIGJAoJpIqZWBAjMERBMcxx1IUBgooBgmoipFQECcwQE0xxHXQgQmCggmCZiakWAwBwBwTTHURcCBCYKCKaJmFoRIDBHQDDNcdSFAIGJAoJpIqZWBAjMERBMcxx1IUBgooBgmoipFQECcwQE0xxHXQgQmCggmCZiakWAwBwBwTTHURcCBCYKCKaJmFoRIDBHQDDNcdSFAIGJAoJpIqZWBAjMERBMcxx1IUBgooBgmoipFQECcwQE0xxHXQgQmChwf0KvDLd7E3ppsb/As7Hr0/13v5V7xr1591Z+5zfzTUeePB7j6CyYEUyfbAwe3YzFrT5q/NBFQJleFggbwf2yS+eaJ5vmHx97kBnB9M44iYdjvDFGnJh3qIHQOEUQPRjj/TH+NoZwGghbU5q8PdZ/Z4wvx3BfbiFNfhn3ZeTJ/8b47ozecYNH0wiVmBvnYfCbca1iipAyvSiQb7i/GKvdz+djEE+4cb0+zQv44mU97FVe+MOq7F0RiHf9ePePJ9QvKg1uWU3+80LMZ9zrt4yv/O3GfXrUE+qMi5UnkPPt7yaCK7flcsxjivW57vmKHV92bc91yz7L0twe65bL+Xq5byxvn9/29nidx4rl7fNeHiOXt+fbPeJ1TMtjX6zZvS73zf1znjXmLwukUcyXy3ltoiKWY8rty20XW178utw/9835cs/tdfk651ftm9ti35zi/PL1vueatYccM2tynrU5z/Ux37Vuub28PCOY4uAJtetElttyOefX1V62Petzvn3c5frl8mX9sn5731y/q265767lXJfzXT2u6n/d/stay9cLXHYdluv3MV/un8s5X57F9rp8nfOr9s1t2/te9zrrtufbdbF917rtuuV+u/bftW5Xj4PX5X/qP7hQAQECBLoEBFOXrL4ECJQFBFOZTiEBAl0CgqlLVl8CBMoCgqlMp5AAgS4BwdQlqy8BAmUBwVSmU0iAQJeAYOqS1ZcAgbKAYCrT3Vhh2//UdmPfkQMT2BKI//M7/zREzrd28XJlAvHL1nHd4tcBTFcLpFHc2+7vq63WsDWuV/wtp6dxg7++OaNZv56yaWfWJPDapm/8Iq/paoH8ywtpdvXetq5F4PUIo39szubzMffRbi2X5vLziL8Q+PUxPtzskk8Fl1fcvi1p8q/xrcd9/cEYca/7GDwQVjzlE9On/weba0V5U6WJqgAAAABJRU5ErkJggg==" + }, + "07a9f89c-6407-4594-9d56-621d5f1e358b": { + "name": "NXP Semiconductros FIDO2 Conformance Testing CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "d61d3b87-3e7c-4aea-9c50-441c371903ad": { + "name": "KeyVault Secp256R1 FIDO2 CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "b92c3f9a-c014-4056-887f-140a2501163b": { + "name": "Security Key by Yubico", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "54d9fee8-e621-4291-8b18-7157b99c5bec": { + "name": "HID Crescendo Enabled", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "20f0be98-9af9-986a-4b42-8eca4acb28e4": { + "name": "Excelsecu eSecu FIDO2 Fingerprint Security Key", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3LWFiODctOTFjM2IzZjBiNzhlIiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6ZWMxZTg3MjEtNzM3YS0wNTRlLWEzYTktNTFkMTMzNDZlZTI5IiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MjE4NWYyYmYtODVmOS1jZjQ3LWFiODctOTFjM2IzZjBiNzhlIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyMTg1ZjJiZi04NWY5LWNmNDctYWI4Ny05MWMzYjNmMGI3OGUiIHN0RXZ0OndoZW49IjIwMTgtMDUtMjNUMTQ6NDA6NTUrMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cykiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+/0VxRQAAGfVJREFUaAXVwXfcn3V97/HX5/v9Xtdv3Ds7JJAIAULYBZmCimDVDlftw23HqYuqPV0WtdbWR63nVG2rnraOtshDrRUfPR3WWS3KVhAZYQoEQkLWndzzN67r+n7e504iKNWO858+n2nuisS/J3G8YZeZ2ZTEImD85+ROO0ZSUfiHJP6FHyIEWBjAwzNw6obI3CykCGaGJNyhLMWwgnropNJICBNUcooi0O8b+xfF6PLAqIMcGod2W+zYD9Fg49rAgb1i0TJTHWGCuo6UheEJdi9mVrSN8cKYq42d+8SKCSO2gAwdIBQQTPx7ZlDVdkkWbzTZcKTI3dhvvrGlueM9d8UTX0Rr+jmoyYCQOMSsBLpAAjLQRxpgxo+RAmlr4ocIZheGkF5lBpL4rwhICXLDfH+gDxeFkHgCCeSwf78hEz/KjMPED5IgRXuRuf20pYBZQ72f7StGH3YmTvxFMhcgAwliARLgGWwGNAfWQqwmhshBcn4sGOA+l8qCxxmQBU3DSZIj8V8TYFC0jYUFbe31dP2y5ZAzTxAS5MZAgPGjzQBB1YDxA9ZZ0KkmcEHImc93Lvi3HfHIkqZejTIgMEAO7l8nxk8h3YLn3YQ0jusM1LyOEM5E4seCgOz/lPYcEI9xQTtxxHg3nukYIL5rEdgOCCj4fgYSsR5qRaejq0Jiuqp4ghQNLw1V4seFAK9FMr5HQLTjQgybMciNg7Hn1pWXfOOh6sSL8PkjMQdLYGGawd7fJXYvR0WfEMAC1BWE4lZ6C/9Mmf6OcuTpSID4kWUG0m7Evem2bc5jho1YOxmPOnMTp2aJ7ICBiY8J/T7QAkYAcZAAQ8Eoc0O2yLbRUUMCM5CMdhv2zTlkI/JjRGARQhHIjXiMGcdKGneM0jKIOx6pV+/LZucj7xAMSPvo6xV49QXSOMzNw8gEdFowMwMjY5DSXprmrRT6B4xViB9dEktuJNqOtHc+8Jj+EDpd2xTajGgAGeMgd/9nYE8I4IIQQCwJgIMLXBANmgySkR2K4Nz9IDw6LzYfLQrjx4YZNDX0ek53LCBxSAp2jplhghY1szZx01XNBXMEthAqQBW95h006QvEEahJtMuXUMQX0FRX02p9hCLNowCersf8PrBV/KfEYcZ/nzjM+AHuEAL/ITlgYMZhBq6bEQvpSUdGHlPVxBVjdo6y4RIgENsEO6JBlpECVLUTghFLQTYcIyMKQZMhG1QNFKX45j1iYtJoJUOV+CEMGAECMA+I/w8CXGCAO1jkv81YIsgOEoeIwyxAXYm5/c6qlYZnaDJH5czJhIBMmOAh3/jlgXVWQz6RYDAYXstC/Rd0lkM5AvI3UHTfRwBqfx4jo1uBL2IR6gDZG0IABO4QI2DgDiYOsQRykIMZP0jgGULicRYAgQvMOEQCMyha4BnkPIEEFqBoQa7AHUIEBDnficjppElxiIDIms6YnZkbaDJYMDz73cgfmWkCRYLJCP0+WAAKHmeAZEgQAgTjkNE2pAgShwjIAozjgZ9BOk+wzsBc7AO+gvikxKP8JwS4GDG4KEXOEqzqtPAA3zHjC4Kt/BcEy4Jx8WibM2JkKooaeAD4CuLbGBQlxBEjZkGf9XVtm4hgCIzZv+XFDz0YNp6NLaxEDmXns0yZEyoo0xnI/oicoakhRMBeg3wTUkn21RgnE8QhrQ4og2cHbQf24qwi2HqSBRqBADMe5w6pgM4YDHqQGzCDkCAVMOyBHCwAAgGxADl4BoscZqAMCGILwjhUPaFswA6C7mFJmnlUHOQZWl1Wj4yyRUEgkBtlyT2tqAN754W5sWRCcKrgDLDjgOUGCoGdGLcC/yp4hB9GEOCYqXZ4bW7sRdF0FGaGIAMpQsCeZYFfM7N3CP7aQHwfATmrRPZLrcivYGyWWVeCtZMgl5rK3pSiPobzh8CA7yMgi1GZXepur4zGpg2rYlnXAjeUhDsPWeTPLfLH1UDafm+mLoyRtv3EZNcmqyxaNCBuvT6euwPxMtRv4+rRG9xIMug0MNQBLNxPa2QLuYFqAMTnA8/noCIAxiEhgucDLPY+TjP4EuNj9+DWJ4RANXM6dN/CyLKzWJwFbyBEQBBLUIDFmQdxXUcq7sTCgGH/KPpzz6AzehIGNA2kNnjewfbbPsrY6vtoTz4fa16IBcgZWiOQ60fYfv+HmFhxB93Rn8Pzy3DdjrGdJam7MXCQBEXkDDPGcgUWwXAGfV1fW0Buay3y87g9v922Ew1bITcwgSAFQ8Jj4H6ZXVFLHwBm+S4HArx49TJ7R9kKxw8WwQKPk6BsQQGWzdYXo/GjdZOjMh82DpMgJjtp9UT8391kF+eGokjCJbIMlxBYrnVku2tvMw9HmvJrBQOWOFAETlnVDh9sWbigccNM1BnEkiAkkLEhBHt3GWwVmd+8d5vzxe/E9Myz7cyLz4fqESiV2Vls+PyeYm2PPk/FMsgHDPozWICqgm7nATy/gNk9r6Eon0d79Ek0FYcICAHEEoEPv8qjD7yTVcddw8R4QzWALBBg+WFmFr/KbHMFU+XzCAmygwUo0x72PfSXPHDn37LlKQ9h1idEwGFm1yo6x7yVsvtG6hkwoDP6NhZmLmfZxhYpXYzXIAGCaCC9i179FzTXQTrhQspN4IvfAuZZkrpdcZCgE2VnezZcImK0Onx1dtb+Lje6eNUK+2DCjq9dhBC05ADSiAXKVjSaRjQixGDHgr3T4FnAr0p82wWdyFtbI+G3TTbeuBAQgBAN5PMjLT53x4O6etsC+84/wdZOYi9tiO8yy7ci3chB4txWyz4S4cQiQOg6vR57TFyVgjyYXSRY1QAOdGJ8qaRrJPtoU3PQuSnYFaPRNmWDjDDYWdV+vRnZ4Gwz22BANZSVnfiqo47ls5POVfPLbO2KUdtMX2AGBQw6E9c0d+1dxdrjNtFOoDhCZ/957HhgK0efC6EG5x4Gi79OSh8gpKcR/dcou6fQn4fskCJQ/z3Ub2BqzU6aPowsO5bh4AJcu/Dmq7QnBvSZZ/vWtzN27Gl0JzcyWATZ9VRzb6bdvobN54qiBWqgGoIitEf3sOfAmxi3SLd9KVV/F63uVzj6LIjFOlRdgAUQEAMMq3vJdhVr1kJuLcMmn4oqoL4ZPIORGHCIGVNEThJgBtn9y8MBrx8ds7cFhXd2ohg2fmPO+nSQ3Qy2D9NkU9kpi42/oGyFi8pIkAtvxMSYnR+K+AkLzYtG23ZBuwxvyz2160aYQZFAUPV7/qmisD9nVLf1+vSne44sQNYVjeztpfHURn4TsM4svM/EiSHBTF/9hUX707Ktj4602IXIN9zVbJ4ai+/fcnS4sBqIxlW0Y3zdvgU+um3ajzjtKP4MbFMtkGnOs783hPDJEOxRSRgciXgbxksFlqKtaKf4wv5QV516rJ60yjmh2m9YEJTsfo9e/8h9BzaewRHzU4QCFFqE8Aa8uomiuIWmD56hLMDig7RHHuSWa7/EsP9RTnn6s4gGi/W1yN5IHOykM7GMhYU3s7j4UsRqilAgPk6Ov0673stR628nhxvI2kh3/CbmF1+LuI3xNeDh6VT9VyGORPlmGv9TJlbtxID54V/Saj8XfCdzexexNtTVWUTfgBmYQTDoDXfQ0zYmWpA2noP7CfhgHyHfjomDkjjMxPpAOA4Dz9wg8X7V+r2RTnz5Yq0Hds/lPxwp7TPBmOO7gkHlXHv3w/6xiSn/+VM2pbdXs/Ykj2I4EKEKW556UvHlmJioemorc0grQQOPHhj6W2nsb8qCx8UIMRi49tdZf1AUXDBWpomFSr9lFs4JCAvM7Zr1S/vzfHzDesMMEDRut873mrcop/cEWB8DzXRP93/qOi/OPzn9amvUnrwwC5ge8tpfBXyNJ7ob9DuYnWjYaZ7FYrZNMcNK2JKCjVdmdBnAgBsf0hHb2LLudaQDI1QVyKCz6mSOmfok7n+M/Et4/QitUeiOgzcg7WDY+z1yPomiXE9jf4hpB6b1pHg54yufwXAAZhANXC+nam4l8B6649BKB8gLMNd7J5Vuo4qREbuMwcJvY2EMi1CMXoSqDthlxAAdzdI0eyk732I4nOOuu2H96tNZtTwxrCAYxAQL+2/CrM/oauhVT6ZVdJhurqetA3QiOKQUje86xYwpwU7Hr20ne0v2dG4/6+vu/ipgG99lgFhiHNI4vUa6HPdv7hvwibFOODUBuRHjIxyRHeoGgkEMsGtG387B31h27GoJEODQbUO3Mu7dnlnZEWXBVLsdO5Y5Xh5eoCiKCDNz+UPT+/zjrZSQwIA6w9pJZzD0awfz+eeSaSwmcpXZNTVqp69ZYb8iB8+OR96dUvxaMEYlGWBLWJKBA3J924zTWOKoXDSnK9uYJAQEgwPN6NW7e2ugzdmQQSwR4NDubMb9r8jFVqI+AfYZot+H+nD0aSz5Bsq30BvsgvANmj3gfhRh+TShuRJ5BYiGAhgh6B6KBAasWH46X7/yc1jrK+x7ADY+8+XE+AcIwwRiSYZ2+UtIZ1A3MxRhAmkzln6fbdsaRIeiOJWDDJBDw4D22LcY9mB2DkJ6MrRgqnMzTX2AbByUkFjSwux0CQyfjm7PDeNh06DUF1p9vZzGpuWAQAYZMMAM3CEA3TZQsHWu1s/UMf/VUd1wSb+GQQ0GmEGIQApff3R/fu3KFdzlAjNQgGYIJ22AZpv40OfhwjMDzz3dLt25x+Ro4+rltiwPIXS4p13yJ1PzRrsFqQV1AwZ0S2M4BEk7DJFlrBiNxYvP54VkVizOiZBsEemngLME44D4nhooDM7iIAODxWgU0ThJAtwgwZfjJXdsDSe2CPkIVAMBMBDQDDkkdU7Euu+iHrwaeAmTozfgwGIFqIf4BKVP0x9C5jq8uY5Q8D3GIcpQlNCdWMnevcv49rc+yrLOIivXrmCyuIzKDRNgPK7JXeBczMAdsPsxu42NR4H78ZThFOoKMEDg7GB0fCsR2Lv/BI5YtxkL8J0br6O3PxMLDkpkDpqk0OkgYrCjrWMj9+3RTdMLevU4TK8eg7IFbpANhAhBWANmcMRyY6SA/oLYvMy31zle2Wu4hCXGYWZQNf73/YpLy5Z2lQFKjNACBehV0CmEAAdiyXndbnrp1unmj8pRzl7fsnbdwM55v3rdlvDoyRsMGjHYATPT0EqwcsKwEFEw3CCHQITV0eyiWuAGEUbKEH7aAQnMDAQOGGAsCYYAA5R9ayfY6Ql7umSU7RrmeHB7/aTbB1Pd55B7G3DLYLs5rA02AUTUgAtSsZHsL2bPgRtoHCxvAFtDsK0YMHlcC08ryL2E6hqL4qAQurgmiUXBsP8wvdYrqPbMsn7l1Zz6HFi25kJy3shgHkLgCQwQICAVsDB7Lb3eblathRBPYXbfCg6yCFZA/5E7Ge6+ndFTYM2G0xlrH0Nv5gBX/eO9PHw3dEY5KClw0LGBcCoYoJFOS+zcmT+9Y5e2r15hdDvG2nFjUIEBBphgUIt2aRy5yrh9u5jtiRPW8Ryv7HfdjIB4TDDDG3v4zl3DfWunjNFWoh2MJkLtEIEA9IYwVjK+6aj4f+gqnLZJN2XF1wzmhRVUDNnaTAMm6gXRzBmt0pA7VQ2rlhc0bmQXMQnPrOkNOc6CiIYHWBCqBMkMY4mExYAlo19l9Tms7WbT9dA/VrTt9BitW1XQsQyJ665ZPHUHzs9igxLxBoyrgQI4HvQBzKZwQVmA5Dy86yYqwfIWdOIFMHICsd0DQTVYhzVXgE1BmAVzzEaAI4EaYz/YDKk6FzpXcMHPPkznKCCtp9ofeZyAwCFyiAkCmeyR1LqdXPWY2QNmJ5DKhDtYgPbYkMXZ/4tFiCuAAz9BM4R+/0Y2n7OLdcdBKjkoyQBjM9A1RBbUiyyun7C7jl4LT1pjzC7AYAhmPEEwkKBqIDsEC78I9qc1jEeE+B530WmFX142mu6qc/6wAxlwAQYIqgxjHVa88qJwxUmrwmmPPly/eqodDySz5XUjYm3FiraWz+4WQSKZEVqgisMETaOOjGyoaHfFcNFGlBkLLDELg+x/Hcw/UgQ7KrsiQg4qZHm20e6W2ZxxSLdpvJ2d+wrs9TlDLA0GkUU1dzQTu6DiGJLNY3wWtA0MpPuBS8HOBYEE84t/QtH6OKuXQf9R8PZTaY+sYvb+BYYzMPKkfRTlPmI8HxzMQAb14MsEu5JQ3IL7y4iD80hjs7hVTO8B91tot2pSTMhABjSQ/XMU5VfBd7M42EIIl7Fm5RyjJXziz6CutvPcN2R6/UTTh8X9H6fV+RuqGaA/Tq5+gl4FqfUNLvz5/aQCJA5KJloW7GQzQxImY+j61oYjuNbN2DcLGJiBeJwBJTB0QQrW3bDC/qAswpuGtSXMOcjEfhkdoCPAXWPHLEvvne9jcj5iAee7hKhqe8bxa8L7WuviKffdnR/+5j360nOeTphMigxAYJV4aoxWFoTKlUEGBnII0X7ZjJcHVAmb2D/jfzbRsu8oWd+zuskgi/Yg+52jId6JGWYQgeyBPZXO3dANFwfRdTEm+TtapR8RzJ6R3eh0wfY3fGbfebddc+zLVlFrI4OqDWqDwAKgA8Bbwf8nKQVC61NUM59h1SS0OtAfvZii9QJMsLhtGckgNnNQ/jLKd0A8h5AXqPt/D91PEFOmGXYJcRliiTajZgr3abJdh/ROxG+hPEWIcyi8H5p3I1+kbqA//B3WroU7bzjAo/fD1BGw7bZPM6yOpCjOoan+lf7sB2lPQQR6u09gZORkHDD7JtUQqiGPSRaYDGZPFocZwkyr+xW/GQwrjEI8rhWMZYKVwOddfMhd58TC3rlqMpxfu2gaUQSjct0WsFcX0iuaaJfKRRa0IqNlN35g6P6zLn0O7CGDo8GeEYM9nRDG6LnPzuc3bZzioeZAXqbxsK1VhOXDSpjZBaXCR8z0Boc5lrizPJq9vSzt0ioTOy1jUGn20Wm/u73Btrfa3D+YtZOzYDTZa3pVmBs29rutksrMkBhPQb+4vh1+TzBlBlm6y4y3J2OF0BaLRr2YSSV3PbjqKV+bmVv3U8TekZgD8dm4303OEAOY/RuR62m1CtA81X4IU9BUmylb78fKZeQ+LH/yZRTDW6mb/eDTiLeT2qMMFobM7x6y+hTIfjTW/zgxnYsDFi6iGZ6C6d9opYzxxzS6imZwBGOj91OH2/DgZIdW+fsU6e20OrDnoROpdSWnPg3WbNpHtrexsDBCqzXHyCQ0DiHB/PRGxiZXYPVecvMQMr5fGhnV+oV5Oy1EDnFA2HGlwluiAcZhxiEu7TXZfULHhEKXE3ha5ayihmhGA9RZ/+TGb7jn78j9ESxeHCwcD2KYRTArkoXnuPjJAH2DtoKlgiUyWPRLJzv6h1gEFqfZ/8h2/c0Jx3NqUZJyA2Z6hdAWI/yrRLdT8EzHNsug0zKiaWeKegnGLQMpDOa5ciTYybULi2bdMv5GnXWhYVeDumZ2tsxOG41K2aGW3SDpJRY0INh5YAgDBwL3rIr7Fqk4DUtgBjG+mex3In0RM8iCfjNgcGDA7COQa5C9iFi8D1tYj9cgQWfiEurp9+LVH5HCvZg5+Bz9Piz0l7GOX4D8FhpbjsQhRiIW76YZ/gIp3oXUYM31pBLm52FQQXtqPa3wv5C/FDOYmYbTnv3bxPYOegsfYd2xMKwyg2qelj2bOh+L6y9ot0RafRG5BuVv4HoYxPdLuw9w3nhbHXcwQIIiQpFgWAl3sMAQ8Yjg9ib7rkQYiYU9H7N1LhEEjXDQ9YtDf380PtNqBc9AI+0I2X8ppXC5sGMdIQlxSBSMGlCYMWg0bda8voU+7dnwDJ0Iew7oY2saf9rqkfhzvVknm8zgzGDhTAEREYNRZdEfautYl1enxHWGyAfcLdtfxzF7Vtm28/p9sSSmZOe4cw4YBzlGPwt3/5cQwpswtg1rJmIRnhmCgaATKmY0ddvn9TwoOQvmOURaTQyXI/8Y8FVcDzB0GM6vYzg4hbXHP5MmP5O8WBITh5hBNQ90foGyfSGevwi2C29Ed/xIyvYFDBePBkpCAnGYZ7B4FmX7M8DloOsw7Samkrn+MXj9FLrpeeDH0TiYgWdojXao6/cSeDbD3q1kb2iXx+P2XFKMiJ8m2DixPA014NxMtlmMJ0jb9tnZZxxnDOfkBBQCw2GjhcVK02WyngVlyeYxTHBcCuECC4zWWVni3mS6rwjcOZe5vsq6Osr2SeIxBpi4buD5xQG7LJm90MFSMCRwiSLSm6n1jwuV3ruyxc0skURrMtDpGidMsZCC/aqyzwq9MkUrzI1GAoxa0E7a45Wu7A/1J2PdcD8CBKpEu9SOnMPL983z5xNtPSsRGGYoAkjgEgm/Z99QHy4jl3eD7R9UjmACOBWJQ8TiPlv+2ft13BbE6YQaCDXuhtkaiuLNoNeQwn5GCqNYPsmyI8aIRaLuQ64bQiEQhxlgEexoTK/joJyh1YGRSRjMC1ETAk+kQExbUH4XhBkIs7hKppYvw2wEr1nimDWAESIMemA2SozPR/58YoQEuACDYJcgB3OWOHAdQfx7afPq8MFqUZ/EaEAKwRZ7feYXKy0eudKyGpsaVkzGSNtgBOTIpptGM2ALKXEAmHfRuKBgifFEBln6lsP/kOuKYPaUoeuoEGwYpHvqxr9eK9zkMDS+TzSsMDoJAuz2rDcOh/nvKsVnWNDxLQiYpt11izJfk7TVzDKPMSAABiHw4N45veThPf6TW9bylLJgw6DCzNiZTNeY+HqWHhLG9EJN3YiU7MBIaa8RgSAlEotfqJ91813941fQ7b+SQMZVAYZkmLWRuhhtygQh1BiLVIsDjExIgPNEDQgDEpAIBrluyE2DmTCWiB+gJgAdjBHMEpKIcQj0aOohZg4YjzGWyJAiUCAHUQMNB0kRcEQbbBa4iR/i/wH3D5PMpd2t5QAAAABJRU5ErkJggg==" + }, + "ab32f0c6-2239-afbb-c470-d2ef4e254db6": { + "name": "TEST (DUMMY RECORD)", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAA+dJREFUeNrEl09oXFUUxn/3vvfmjzOdmZcmcSakmUyGqoQolBQXMV2J/7DulLYGFHFRN0J0IQhSUAp22Y0utBZLsaJYMGhATV1INxJr1ZKmNqUYM5kYk2kmMzGZmffvuhhJtULmjQ7NWb533zkf3znfd94V05l+gMeBV4F7uT1xCTgGjIvpTP9DwFdsTzwsgeNsXxyXQHYbAWR1wAaCvj8RApTCW9/ALZfBdRGBAFoijggGQalmANg64Pmureu4xSJ2YZlAupfonvsQwSBucZXq5Su4+XmM7l2IUAhc109KT2+muL34OzIcouvYUcxnRzCSyc331anLFN5+l5V3TiITcXTTRPkAIaYz/SUg1uigWywS6E2T/Xocra0NgI3vvseanSPY10t4cA8AxQ8+IvfcYbQ2ExmJNGpJ2T8Dmo5yXaz5BfSNCrnDL7L25TmUW0VqISLDQ/ScPoE5cgCnUCA/+jLBvt2tY0DoOs7KCgiJnohT+2UWoyuFCBgoy6Gau0pkYC+7J88jwyFm9u6jNnMNvX3nlgxIvwwox0FLJJABA7dUJtCbRug6eAqha4SzA6xPXaD4/mkAYvsfw11bbZhXNqVaz0MEg8hoBLxbxKMUGiHWv50EINiXBtwWA5ASZVko2wYp/+UPChstGq1jrVq+UurNGJCyLFTNQjkO0vMQ4XCdCSlRGxsoPBIHnwSg8sOPCAItBADYuTl6Tr0HmkZ+9BWklAjDQFkWXqVK6sgbRPY9gLN8g9LZMfTOzha1QErsXI7I0BDmM09jjhwgcv8gTuFGne5SmUAmTfL11wDIPf8CzvIyWmxHixhwXJRtkzx6BIC1Lyb445vzmxLTEgmsuXlWTp7Cmp2j/NnnBPqyLXJCIbDzeSLDQ2TPjQOKmcFhqlPTGLu66zMgBHgKZ2kJ5XkYqeTm0moQPpxQKbzaOuahAwCUPhlj/eIkoczdN6WoFEjQOtoRQtx81goVeJUKgVQPsf2PArB69lMEBgjg7zUUCNmcqn0NoVsqE+y/B/3OTpRlU/npEnrbzmb3/n8HoCpVgtlMfeVe+RlncQkZDrXsl6gxAFyM7q66D8wv4K6t1XdAi8JHJg8tYdbbUShQc8rwq3vLAPwztDYTvb0DZVutASDvCAMQfeRB7jrzMXJHdGttjY2z8uEZjM5UKwAoMOrHjGSSxKGnGvvWcoGlE29hkPr/RqRqNYx0D3pHu+++Or8tYucX6n/JPoxoy0GUkSi1q9eoXLjoG4AWj6OZJsqxG4pAb9QG5dho8RhaPNbUdPsoDmBI4Po23oyuS+ClbQQwqgMTwBN/Xc8HblPhKeBNYOLPAQDIsXqbsqZKGwAAAABJRU5ErkJggg==" + }, + "30b5035e-d297-4fc1-b00b-addc96ba6a97": { + "name": "OneSpan FIDO Touch", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "6d44ba9b-f6ec-2e49-b930-0c8fe920cb73": { + "name": "Security Key by Yubico with NFC", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "e416201b-afeb-41ca-a03d-2281c28322aa": { + "name": "ATKey.Pro CTAP2.1", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "cfcb13a2-244f-4b36-9077-82b79d6a7de7": { + "name": "USB/NFC Passcode Authenticator", + "icon_light": null, + "icon_dark": null + }, + "91ad6b93-264b-4987-8737-3a690cad6917": { + "name": "Token Ring FIDO2 Authenticator", + "icon_light": null, + "icon_dark": null + }, + "9f77e279-a6e2-4d58-b700-31e5943c6a98": { + "name": "Hyper FIDO Pro", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAI0AAAAWCAYAAAD9/x8lAAAABHNCSVQICAgIfAhkiAAAB3FJREFUaIHtmk1y29gRx38NItLSzAnMnMDMNkmV6aqpynJ4A9MnMCSSVaG0MLwQsRBlwScQdYKRVlmlRG5mG+oEQ50g1C5USHQWj/h+/NBEtmcm+q8IvEa/fkD3v/v1Y4VNOAxa/Pm7Kj/+Y1oa8/wqf/nr3/nTd3fW8Wf8ZuGuHWn3mwgXwBvreGUvBBpAg8PgHZ96wy9i4TO+Ldr9JiKvVldjBr2RYxXsntSBiw2Khoi8Ta4dLjgMWk9o6jN+Cej0PURmwBiJromo0QkaZafpntSJ5AaR6gZFb0v3nx3nNwipMuiNUB2iElKJJqD1fHry/CoqF2sdxjjF+do5jOOwMVVl6U6ia06PJ7nxTtAAXq+uxiyY4pI66WL+mdCf5Z7pntRR5/tUhkt+F1WJ5BUitZINqlOWMibspbVYp++BvFhrd6w37E1NYFVeI2p5TzJh8e9xycZ4bSqvcs+JjviP3CW2PMaOGF5Qo6KvS2sVHXF6NMbzq7j7783aZcbZ3z7n5LyglrzjiLvk+0WYOUSqqNYYHE/oBM2807h7VyD1zJ1rBr1RsuBSytIDVFoIr5JbDhe0+zPOjq6sCxY8YqdQR4BJQaIBfFj9/gjzEPYPAPMiK3t/APKMFomHJI51D/PP6N4QkdfYIGKquVwtJuuDIYbLGJiiEiJq141CZW/GYXCQ6O6e1ImcH4AaogVxAVfHq3U/zg6AdhAivAexmCLQCeKa1DfqFSDvNC61ZNzRMWDsFuqrJQ1BjHOhszQ9tftDyLxk5ZbFvJUsWvWHgkkfGRyFLOcNlNvC2MWqLvrfYSI2TK5F3hrjV/CCWi5dRnjWKLfB4SKn66kgUkX0HM83jBLJFcLTz9MJfOMwXwhLQtpBCPITyE+4tFg8DA3THAatTKQah1nOG4T+DM+vlmoc1UvOjoxnGpkGlf1RwjgiVZQL4I9PYvyg59PutxB5CUAFD/DMb/WTKFO949NROTWqXiISU24NJ8OYDg3iyEofOAApMiAs5uV7Wd1ZlhSp4u7XgVFi9zrdomucfIsdSjMhGNU7IC5c87LGjsfDpECveNs1karnGXq7Z0kziVZ3fwhkc/c1Z0cpA50eT6yOg9TpBD6Dnv+zDC5CxV+1AAB9i+f7sF/NObuIvRAXmSZpFqDTbyWs6tgYQCY5+U3I6x7RDpq5dF3EQq5y9chm5ZvtyM4j0lor2wl2m25HuFTUz7FIhJdflFbTSOaW5SplxUVzzCahP6N70kKdf6aP6nviXGmD8pJuP18bRLy0pWc+9YbJxzZR7KFaS51dxwyOdvvQ3xIVbmj3fZYP1zunURu6J3Wy5dGuTv4EcBFpZq7v1+58iinL3bspFM1wejyh0x8nUSxSxQtqayNLaKEFdrA5TDroAzfGHn2f3+XJbs4ZUcvVbvEOIY+bUnSqzjg7+v1G3SoNsLCMSWGGEYUayBB3H9rBEOFywwcv22GCo4E69h3uV4BDvCsBUP61Rs6SssSeJ7VA9ztT8Q4wL/caoFRjbabxFiojVEaZ+gPgnmhu3+WVdKxpQ2R1Z1lV9S6xafngoXppfdY4xtOk8K8EFzTDDNQ4DFp5tpEZEjUIj1dbvP4Q+N6iK+4xZIu+8cbZVe+QQqQrtXzhWMACD7cw/3IDy6ydm1ucqGVNEYYZCs6+rli14hpHU5vMHC28wMfVJopXWOMHvGBYCjCbHVHRrq8PFyVESOla9JzuySRpui3m6Ys1PYFsN/g++WX6OIUew5aPKTIsFcom6j7YH8AwV7uf0r3yeSubZXc4u+R+Y9euNcIbVKuIZFsSYalpGdtu2gfh6n1dETO96ZXk17HJDrMrSq83lQFbZbW+pS7IwVk14a4zhpotdtxniR3GbMvzPQGJTEPK1sdRPn+x4iwbfcJ2Boh3OF/KnuI7RLc36Aa9EZpxkuiRfRzzXdKgrWwKtIKsm2mOml5Spt1i2eIXYPo0i3mLyt4koUyRKhE3dE/ecHo84TBo5XobABHv+HQ8sZ5VKbec9Ur7+18P9JxOUHZGiQ6sDALmHbr7U+BFrt1gjjjKTqTUcg2/SmTRu8UO1atMgd1aHdFMrLIwIi0rPtAO3iJMUa1Dtl7TrYFlnMZsl5urYs7QZew47b5nIidDXxFp+z1yhgjZovSO5UNj28S/bKwr8jfsWEJ/RqfvJ8cAqu/xgiFKleSIIDtFVq9eMrA54xY7luLj0iT7zYpzxbIS+ajTSGWpATUkY4hyu/b4J4P07On0eEL3pIE6eccpdktVL3Nd13wj6x5Hm5xt6D+oTJLzF1tRFzFdnX+sL/p2kdk2T/mBzUU7pJ3brO5sN3dwFNLu1xFqCCYNLBji8hE0PluqAy9WG5AZEVf5LvYj7Ah7U7ygTgUP0XqqG+MAwpTFKgWeHk+MrPog9fx30zHIiOU8LE5lnb50x9Bp6jhZmOODfF+lE2RbTG++ZpPpGd8G5f/TnB5PVgXufX5AxyWHySLi3bPD/H/A/s+9ouMotywemlZZI3Dw/HfPZxh0T+p0+qPkiN+GTv9XvEt6xs/BfwGhhmnYcaydgQAAAABJRU5ErkJggg==", + "icon_dark": "data:image/png;base64,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" + }, + "0bb43545-fd2c-4185-87dd-feb0b2916ace": { + "name": "Security Key NFC by Yubico - Enterprise Edition", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "73402251-f2a8-4f03-873e-3cb6db604b03": { + "name": "uTrust FIDO2 Security Key", + "icon_light": "data:image/png;base64,wolQTkcNChoKAAAADUlIRFIAAABkAAAADggGAAAAw5nCjcK5aAAAAAFzUkdCAMKuw44cw6kAAAAEZ0FNQQAAwrHCjwvDvGEFAAAACXBIWXMAABJ0AAASdAHDnmYfeAAAB8OfaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49IsOvwrvCvyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzE0OCA3OS4xNjQwMzYsIDIwMTkvMDgvMTMtMDE6MDY6NTcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjEuMSAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjAtMDQtMTBUMTE6NDY6MTYtMDQ6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIwLTA0LTEwVDExOjQ2OjMyLTA0OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIwLTA0LTEwVDExOjQ2OjMyLTA0OjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjUyM2FkMzNkLTkwMjMtNGNlNS05MGJmLWUzZmExZDdjMGFlNiIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjBhMTFlZTdmLWQ5ZTQtYWM0NC1hM2I2LTllZmVkYTA0NDA5ZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOmI4ZGRmYTA5LTdiM2MtNDMwMy1iNTlmLWE2MTQyZTdiMTJhYSI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6YjhkZGZhMDktN2IzYy00MzAzLWI1OWYtYTYxNDJlN2IxMmFhIiBzdEV2dDp3aGVuPSIyMDIwLTA0LTEwVDExOjQ2OjE2LTA0OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjEuMSAoTWFjaW50b3NoKSIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY29udmVydGVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJmcm9tIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3AgdG8gaW1hZ2UvcG5nIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo1MjNhZDMzZC05MDIzLTRjZTUtOTBiZi1lM2ZhMWQ3YzBhZTYiIHN0RXZ0OndoZW49IjIwMjAtMDQtMTBUMTE6NDY6MzItMDQ6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyMS4xIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8eG1wTU06SW5ncmVkaWVudHM+IDxyZGY6QmFnPiA8cmRmOmxpIHN0UmVmOmxpbmtGb3JtPSJSZWZlcmVuY2VT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", + "icon_dark": "data:image/png;base64,wolQTkcNChoKAAAADUlIRFIAAABkAAAADggGAAAAw5nCjcK5aAAAAAFzUkdCAMKuw44cw6kAAAAEZ0FNQQAAwrHCjwvDvGEFAAAACXBIWXMAABJ0AAASdAHDnmYfeAAAB8OfaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49IsOvwrvCvyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzE0OCA3OS4xNjQwMzYsIDIwMTkvMDgvMTMtMDE6MDY6NTcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjEuMSAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjAtMDQtMTBUMTE6NDY6MTYtMDQ6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIwLTA0LTEwVDExOjQ2OjMyLTA0OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIwLTA0LTEwVDExOjQ2OjMyLTA0OjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjUyM2FkMzNkLTkwMjMtNGNlNS05MGJmLWUzZmExZDdjMGFlNiIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjBhMTFlZTdmLWQ5ZTQtYWM0NC1hM2I2LTllZmVkYTA0NDA5ZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOmI4ZGRmYTA5LTdiM2MtNDMwMy1iNTlmLWE2MTQyZTdiMTJhYSI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6YjhkZGZhMDktN2IzYy00MzAzLWI1OWYtYTYxNDJlN2IxMmFhIiBzdEV2dDp3aGVuPSIyMDIwLTA0LTEwVDExOjQ2OjE2LTA0OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjEuMSAoTWFjaW50b3NoKSIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY29udmVydGVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJmcm9tIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3AgdG8gaW1hZ2UvcG5nIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo1MjNhZDMzZC05MDIzLTRjZTUtOTBiZi1lM2ZhMWQ3YzBhZTYiIHN0RXZ0OndoZW49IjIwMjAtMDQtMTBUMTE6NDY6MzItMDQ6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyMS4xIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8eG1wTU06SW5ncmVkaWVudHM+IDxyZGY6QmFnPiA8cmRmOmxpIHN0UmVmOmxpbmtGb3JtPSJSZWZlcmVuY2VT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" + }, + "c1f9a0bc-1dd2-404a-b27f-8e29047a43fd": { + "name": "YubiKey 5 FIPS Series with NFC", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "504d7149-4e4c-3841-4555-55445a677357": { + "name": "WiSECURE AuthTron USB FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "5fdb81b8-53f0-4967-a881-f5ec26fe4d18": { + "name": "VinCSS FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "2d3bec26-15ee-4f5d-88b2-53622490270b": { + "name": "HID Crescendo Key V2", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "cb69481e-8ff7-4039-93ec-0a2729a154a8": { + "name": "YubiKey 5 Series", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "0076631b-d4a0-427f-5773-0ec71c9e0279": { + "name": "HYPR FIDO2 Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAACNgAAAjYCAYAAAAADILPAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABANpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQ1IDc5LjE2MzQ5OSwgMjAxOC8wOC8xMy0xNjo0MDoyMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ1dWlkOjVEMjA4OTI0OTNCRkRCMTE5MTRBODU5MEQzMTUwOEM4IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkQ4RThERjcwNzM1NzExRTk5MTU1RUU2NEM3MEEwNDExIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkQ4RThERjZGNzM1NzExRTk5MTU1RUU2NEM3MEEwNDExIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE5IChNYWNpbnRvc2gpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MTBhMjJkMGUtMjUzNy00ZjU1LWEzNTctZjE3Yzk0Y2ZlNTkxIiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6OTg5YTAzY2YtNjlhZS0xZDQwLWI0OWYtOWQxMTFlMGU2YjM1Ii8+IDxkYzp0aXRsZT4gPHJkZjpBbHQ+IDxyZGY6bGkgeG1sOmxhbmc9IngtZGVmYXVsdCI+UHJpbnQ8L3JkZjpsaT4gPC9yZGY6QWx0PiA8L2RjOnRpdGxlPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pl2Dyx0AAJydSURBVHja7N3/bVxVGoDhE0QBKWEaQEoJLiEdrDvYNICSiAIQFZBUsNkKGCrAiAIYKiBbgXcOMxP/UPISknjGnnke6Uj2hD/gs3WUe+/LuY8uLy8HAAAAAAAAAADwfl8ZAQAAAAAAAAAAfJjABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACC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DQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAD/z94d1caNhmEY9UWJFEIgBEIgDIQwaBkEgpdBIHQZDIRACIT9LY+0lXb1tE1mkrHnHOmT79/rR/4BAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAA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AAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgC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NgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAARtX8nE+AUck4AAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAACNgAAAjYCAYAAAAADILPAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABANpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQ1IDc5LjE2MzQ5OSwgMjAxOC8wOC8xMy0xNjo0MDoyMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ1dWlkOjVEMjA4OTI0OTNCRkRCMTE5MTRBODU5MEQzMTUwOEM4IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkQ4RThERjcwNzM1NzExRTk5MTU1RUU2NEM3MEEwNDExIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkQ4RThERjZGNzM1NzExRTk5MTU1RUU2NEM3MEEwNDExIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE5IChNYWNpbnRvc2gpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MTBhMjJkMGUtMjUzNy00ZjU1LWEzNTctZjE3Yzk0Y2ZlNTkxIiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6OTg5YTAzY2YtNjlhZS0xZDQwLWI0OWYtOWQxMTFlMGU2YjM1Ii8+IDxkYzp0aXRsZT4gPHJkZjpBbHQ+IDxyZGY6bGkgeG1sOmxhbmc9IngtZGVmYXVsdCI+UHJpbnQ8L3JkZjpsaT4gPC9yZGY6QWx0PiA8L2RjOnRpdGxlPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pl2Dyx0AAJydSURBVHja7N3/bVxVGoDhE0QBKWEaQEoJLiEdrDvYNICSiAIQFZBUsNkKGCrAiAIYKiBbgXcOMxP/UPISknjGnnke6Uj2hD/gs3WUe+/LuY8uLy8HAAAAAAAAAADwfl8ZAQAAAAAAAAAAfJjABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACCwAYAAAAAAAAAAILABgAAAAAAAAAAgsAGAAAAAAAAAACC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DQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAAAAAAAsCCwAQAAAAAAAACABYENAAAAAAAAAAAsCGwAAAAAAAAAAGBBYAMAAAAAAAAAAAsCGwAAAAAAAAAAWBDYAAAAAAAAAADAgsAGAAAAAAAAAAAWBDYAAAAAAAAAALAgsAEAAAAAAAAAgAWBDQAAAAAAAAAALAhsAAAAAAAAAABgQWADAAAAAAAAAAALAhsAAAAAAAAAAFgQ2AAAAAAAAAAAwILABgAAAAAAAAAAFgQ2AAAAAAAAAACwILABAAAAAAAAAIAFgQ0AAAAAAAAAACwIbAAAAAAAAAAAYEFgAwAAAAAAAAAACwIbAAAAAAAAAABYENgAAAAAAAAAAMCCwAYAAAAAAAAAABYENgAAAAD/z94d1caNhmEY9UWJFEIgBEIgDIQwaBkEgpdBIHQZDIRACIT9LY+0lXb1tE1mkrHnHOmT79/rR/4BAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAA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AAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgCGwAAAAAAAAAACAIbAAAAAAAAAAAIAhsAAAAAAAAAAAgC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NgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAAhmADAAAAAAAAAABDsAEAAAAAAAAAgCHYAAAAAAAAAADAEGwAAAAAAAAAAGAINgAAAAAAAAAAMAQbAAAAAAAAAAAYgg0AAAAAAAAAAAzBBgAAAAAAAAAARtX8nE+AUck4AAAAAElFTkSuQmCC" + }, + "d7a423ad-3e19-4492-9200-78137dccc136": { + "name": "VivoKey Apex FIDO2", + "icon_light": "data:image/png;base64,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cAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgeG1sbnM6R0lNUD0iaHR0cDovL3d3dy5naW1wLm9yZy94bXAvIgogICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgIHhtcE1NOkRvY3VtZW50SUQ9ImdpbXA6ZG9jaWQ6Z2ltcDo2OWExYmMwNS00M2JkLTRhMjQtOTQ3MC01NGM4YTI3YzcxYmMiCiAgIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDJmZGJlZmYtMTJlOS00Mzk4LThkMDQtMDU0MzExYWZlYjE2IgogICB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6ZGNjNjkyYzctYjJiNS00NWFlLWFmOGQtZjAyZWUwYTI5ZDU1IgogICBkYzpGb3JtYXQ9ImltYWdlL3BuZyIKICAgR0lNUDpBUEk9IjIuMCIKICAgR0lNUDpQbGF0Zm9ybT0iV2luZG93cyIKICAgR0lNUDpUaW1lU3RhbXA9IjE2NjAxNTI5MDEwMzU3ODAiCiAgIEdJTVA6VmVyc2lvbj0iMi4xMC4zMCIKICAgdGlmZjpPcmllbnRhdGlvbj0iMSIKICAgeG1wOkNyZWF0b3JUb29sPSJHSU1QIDIuMTAiPgogICA8eG1wTU06SGlzdG9yeT4KICAgIDxyZGY6U2VxPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJzYXZlZCIKICAgICAgc3RFdnQ6Y2hhbmdlZD0iLyIKICAgICAgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDphYjljYTRkNC0xMDQ3LTRjZGQtODAyNi00OTI1YjY5ODNjYmMiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkdpbXAgMi4xMCAoV2luZG93cykiCiAgICAgIHN0RXZ0OndoZW49IjIwMjItMDgtMTBUMTA6MzU6MDEiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgIAo8P3hwYWNrZXQgZW5kPSJ3Ij8+6HMtNwAAAAZiS0dEAP8AAABBMvwN9QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+YIChEjAPBJR7wAAAkDSURBVFjDrZZ7bFP3Fcc/92HHdpz4FcdxDEnIC5KQ8AyQUJJQCpQWNlhbtI2qa9Vu09ROk/bf1D/2R6f9W01bJ23a1kntimgR7WhXSimlkJIGSElDAiHk6RDHeThx7Nj4fe/+IJiYAK2mff+6uufc3/mec77n3J+gqqoKoKqQUhSSKYVUSiGRSuIL3MLrD+H1h5gORvCHIgTCMW7FEiiKSlmBmcfWljI84efSoBedRsZs1JFvyqay0EaZ04qIgiiKiJKILIrI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", + "icon_dark": "data:image/png;base64,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cAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgeG1sbnM6R0lNUD0iaHR0cDovL3d3dy5naW1wLm9yZy94bXAvIgogICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgIHhtcE1NOkRvY3VtZW50SUQ9ImdpbXA6ZG9jaWQ6Z2ltcDo2OWExYmMwNS00M2JkLTRhMjQtOTQ3MC01NGM4YTI3YzcxYmMiCiAgIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDJmZGJlZmYtMTJlOS00Mzk4LThkMDQtMDU0MzExYWZlYjE2IgogICB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6ZGNjNjkyYzctYjJiNS00NWFlLWFmOGQtZjAyZWUwYTI5ZDU1IgogICBkYzpGb3JtYXQ9ImltYWdlL3BuZyIKICAgR0lNUDpBUEk9IjIuMCIKICAgR0lNUDpQbGF0Zm9ybT0iV2luZG93cyIKICAgR0lNUDpUaW1lU3RhbXA9IjE2NjAxNTI5MDEwMzU3ODAiCiAgIEdJTVA6VmVyc2lvbj0iMi4xMC4zMCIKICAgdGlmZjpPcmllbnRhdGlvbj0iMSIKICAgeG1wOkNyZWF0b3JUb29sPSJHSU1QIDIuMTAiPgogICA8eG1wTU06SGlzdG9yeT4KICAgIDxyZGY6U2VxPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJzYXZlZCIKICAgICAgc3RFdnQ6Y2hhbmdlZD0iLyIKICAgICAgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDphYjljYTRkNC0xMDQ3LTRjZGQtODAyNi00OTI1YjY5ODNjYmMiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkdpbXAgMi4xMCAoV2luZG93cykiCiAgICAgIHN0RXZ0OndoZW49IjIwMjItMDgtMTBUMTA6MzU6MDEiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgIAo8P3hwYWNrZXQgZW5kPSJ3Ij8+6HMtNwAAAAZiS0dEAP8AAABBMvwN9QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+YIChEjAPBJR7wAAAkDSURBVFjDrZZ7bFP3Fcc/92HHdpz4FcdxDEnIC5KQ8AyQUJJQCpQWNlhbtI2qa9Vu09ROk/bf1D/2R6f9W01bJ23a1kntimgR7WhXSimlkJIGSElDAiHk6RDHeThx7Nj4fe/+IJiYAK2mff+6uufc3/mec77n3J+gqqoKoKqQUhSSKYVUSiGRSuIL3MLrD+H1h5gORvCHIgTCMW7FEiiKSlmBmcfWljI84efSoBedRsZs1JFvyqay0EaZ04qIgiiKiJKILIrI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" + }, + "ba76a271-6eb6-4171-874d-b6428dbe3437": { + "name": "ATKey.ProS", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "ee882879-721c-4913-9775-3dfcce97072a": { + "name": "YubiKey 5 Series", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "8876631b-d4a0-427f-5773-0ec71c9e0279": { + "name": "Solo Secp256R1 FIDO2 CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "fec067a1-f1d0-4c5e-b4c0-cc3237475461": { + "name": "KX701 SmartToken FIDO", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "b267239b-954f-4041-a01b-ee4f33c145b6": { + "name": "authenton1 - CTAP2.1", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAbAAAAGxCAYAAAADEuOPAAAACXBIWXMAABcSAAAXEgFnn9JSAAAFFmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNi4wLWMwMDMgNzkuMTY0NTI3LCAyMDIwLzEwLzE1LTE3OjQ4OjMyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjIuMSAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIxLTExLTIwVDE0OjQwOjUwKzAxOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMy0wNC0xNlQxODoxOTo1OSswMjowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMy0wNC0xNlQxODoxOTo1OSswMjowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2NGRiZjU4ZC05OTY4LTg4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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAbAAAAGxCAYAAAADEuOPAAAACXBIWXMAABcSAAAXEgFnn9JSAAAFFmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNi4wLWMwMDMgNzkuMTY0NTI3LCAyMDIwLzEwLzE1LTE3OjQ4OjMyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjIuMSAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIxLTExLTIwVDE0OjQwOjUwKzAxOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMy0wNC0xNlQxODoxOTo1OSswMjowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMy0wNC0xNlQxODoxOTo1OSswMjowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2NGRiZjU4ZC05OTY4LTg4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" + }, + "b50d5e0a-7f81-4959-9b12-f45407407503": { + "name": "IDPrime 3940 FIDO", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "8c97a730-3f7b-41a6-87d6-1e9b62bda6f0": { + "name": "FT-JCOS FIDO Fingerprint Card", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC" + }, + "a1f52be5-dfab-4364-b51c-2bd496b14a56": { + "name": "OCTATCO EzFinger2 FIDO2 AUTHENTICATOR", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "3e078ffd-4c54-4586-8baa-a77da113aec5": { + "name": "Hideez Key 3 FIDO2", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAAG0OVFdAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxMjFDOUI2OTVBMDExMUU1QkRBREQwQkJFMUZFRjhGRCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxMjFDOUI2QTVBMDExMUU1QkRBREQwQkJFMUZFRjhGRCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjEyMUM5QjY3NUEwMTExRTVCREFERDBCQkUxRkVGOEZEIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjEyMUM5QjY4NUEwMTExRTVCREFERDBCQkUxRkVGOEZEIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+vr5XIgAAE/9JREFUeNpiDDl6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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAAG0OVFdAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxMjFDOUI2OTVBMDExMUU1QkRBREQwQkJFMUZFRjhGRCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxMjFDOUI2QTVBMDExMUU1QkRBREQwQkJFMUZFRjhGRCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjEyMUM5QjY3NUEwMTExRTVCREFERDBCQkUxRkVGOEZEIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjEyMUM5QjY4NUEwMTExRTVCREFERDBCQkUxRkVGOEZEIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+vr5XIgAAE/9JREFUeNpiDDl6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" + }, + "ec31b4cc-2acc-4b8e-9c01-bade00ccbe26": { + "name": "KeyXentic FIDO2 Secp256R1 FIDO2 CTAP2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "d41f5a69-b817-4144-a13c-9ebd6d9254d6": { + "name": "ATKey.Card CTAP2.0", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxEAAAsRAX9kX5EAAAU0SURBVGhD7Vpbc9NGFD62bMWOLzi2kwJ2LpAWSgt0IEBvT33tdKYz7Vt/YB86w2/gpZ02hYdOAk+FaSBpIDeH2CW+yz3fareR09iyoks8jr+ZM9autOv99O3Zc7RS6KeVtQ6dI4Tl77nBmPCoY0x41DEmPOoYKA7Xmm0yaLjDdZhCFItqstQbtoRB9vubc6RHwtQZUs6hEFGjZdDDp69sSdtOaSiraxpFwmGKasNpGJvOv4PMwoF8uDOs0low6Bhtp/Rhs0U/3L5CUZ7SwPPdCm2/q5KGeXSGaDPBmUSc3s+nRLnZatOPK2s0GY2Ici84Jvzryx36c6/C0+hsCbeMDn2QS9Hn89OiPChhx2EpzMqC7Em+FKRhDBiLUzgm7BYGT8U2qwPDcdAIlDBIxiIapSeiwnCMuiARGGGsom3DoG8/mqWvPywIwzHqgowCgRFuspK3Lk7J0hFQh3NBIRDCULDFSt6+9H/CqMO5oFQOhDAU/HgmI0tE7xotqnK4U8A5hJkg4DthKGewgveKOVlDtPJ6n/7Y3JcloqVCNjCVfScM5a7l07JkhqXnpbIwRTDE8fT6dDoQlX0lrHx3yaLuKqsbDoWFrbw5Uvnu5VwgKvtKGDF2kdM/PM0orG69pSgyNbbVN29lLYnsCdf6HZd9Iwyl6u02PSjmZQ3Rs619fkw3p7AwWadwv5ATbfxU2TfCeJpZyCSFcgpP/i6RxmobrCIMx082SvIskc6ZF9qgrV/whTAUarQN+mzOfJIBXuyVKaVHKMmWkIbj1ESEz1XkVUQPZnOirV8q+0IYCs2mJ7u2WxZzafru5jx9c6PYZaiD7ypM6lEqclu/VPacMJRpskLWldkpltiX0YcfKntOGItsgRW6ENNljXNk4rrow48F2/GOx/KrXXpRqnQtRlYgrOC53BSn0xWS6qzaV1feo8sXJkV58+CQHv21RROWvhCLeVj/9aH12FnBDFjMpujTOTMK+Lbj0Q/IouLst1enkrQwlRAZFkjCH4UJyaz3V24GyPO4Fm3QFn2gL683CTwjDH+r8V3+cn6a7s/mxQo9l0mIemzFmIYrrYqZdeo8rkUbtEUfX/Av+vTSlz0jDPGy7Hv5REzWEP28tt1z6p+EKE//X17uyBLRdDIm+vTSlz0hjPE0OENCPqyw/U+VyvVWl552gN8e1BrctiZriO5cyrK/ssqy7BbeEOYpl+L4WZCLEbC8vifeBiCFHBS4Fm85Hm/syhqiIk/xJPft1bT2hDDe69zlZ1qF0mGdStW69FlnQJtdtGdTuMN9I/vyAq4JYxXVtRDN86qq8Nv6DocazZG6CmiDtsvrRyovcN/i3ZEHKrsmjDuPFVWhLHyw3jN+DgK03WI/Rl8K9zxS2RVh3HGocZUTAAUoE5NJihtMcB+/b+zJkpmLI0Fxq7KrkSHb+cSyE4nNudeVqoipboGXdZvlQ9Gnwq2LGfGfbnBqwlg1xS5FNkl1Tg7wfLvMvou6fr5rjcv9YjT6wPnHFl++MZMRbyvcqOwqlwbpGq/QZiQ2CVhz5+PAQOM84Igk2mK1qnyzes0I9I82aX4QwTGuwxcJTc63seEXeC4NFZDvxvlPYP3IAhgwCJZrTWH9yALoH+dxbYWTmAP+Bdl+M8gOrgifBiCAVRjWj6wCyKnrYW7IAo4JY4phOmHxOEvDGE7jy+NPHo7jOOFhhaeLllu/CQKDjtGWML5ww6Mftl5O8qVhMIwNaSfGagfbKQ2cq08PRw3DvRL5gDHhUceY8KhjTHi0QfQv3WxwqZwG02wAAAAASUVORK5CYII=", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxEAAAsRAX9kX5EAAAU0SURBVGhD7Vpbc9NGFD62bMWOLzi2kwJ2LpAWSgt0IEBvT33tdKYz7Vt/YB86w2/gpZ02hYdOAk+FaSBpIDeH2CW+yz3fareR09iyoks8jr+ZM9autOv99O3Zc7RS6KeVtQ6dI4Tl77nBmPCoY0x41DEmPOoYKA7Xmm0yaLjDdZhCFItqstQbtoRB9vubc6RHwtQZUs6hEFGjZdDDp69sSdtOaSiraxpFwmGKasNpGJvOv4PMwoF8uDOs0low6Bhtp/Rhs0U/3L5CUZ7SwPPdCm2/q5KGeXSGaDPBmUSc3s+nRLnZatOPK2s0GY2Ici84Jvzryx36c6/C0+hsCbeMDn2QS9Hn89OiPChhx2EpzMqC7Em+FKRhDBiLUzgm7BYGT8U2qwPDcdAIlDBIxiIapSeiwnCMuiARGGGsom3DoG8/mqWvPywIwzHqgowCgRFuspK3Lk7J0hFQh3NBIRDCULDFSt6+9H/CqMO5oFQOhDAU/HgmI0tE7xotqnK4U8A5hJkg4DthKGewgveKOVlDtPJ6n/7Y3JcloqVCNjCVfScM5a7l07JkhqXnpbIwRTDE8fT6dDoQlX0lrHx3yaLuKqsbDoWFrbw5Uvnu5VwgKvtKGDF2kdM/PM0orG69pSgyNbbVN29lLYnsCdf6HZd9Iwyl6u02PSjmZQ3Rs619fkw3p7AwWadwv5ATbfxU2TfCeJpZyCSFcgpP/i6RxmobrCIMx082SvIskc6ZF9qgrV/whTAUarQN+mzOfJIBXuyVKaVHKMmWkIbj1ESEz1XkVUQPZnOirV8q+0IYCs2mJ7u2WxZzafru5jx9c6PYZaiD7ypM6lEqclu/VPacMJRpskLWldkpltiX0YcfKntOGItsgRW6ENNljXNk4rrow48F2/GOx/KrXXpRqnQtRlYgrOC53BSn0xWS6qzaV1feo8sXJkV58+CQHv21RROWvhCLeVj/9aH12FnBDFjMpujTOTMK+Lbj0Q/IouLst1enkrQwlRAZFkjCH4UJyaz3V24GyPO4Fm3QFn2gL683CTwjDH+r8V3+cn6a7s/mxQo9l0mIemzFmIYrrYqZdeo8rkUbtEUfX/Av+vTSlz0jDPGy7Hv5REzWEP28tt1z6p+EKE//X17uyBLRdDIm+vTSlz0hjPE0OENCPqyw/U+VyvVWl552gN8e1BrctiZriO5cyrK/ssqy7BbeEOYpl+L4WZCLEbC8vifeBiCFHBS4Fm85Hm/syhqiIk/xJPft1bT2hDDe69zlZ1qF0mGdStW69FlnQJtdtGdTuMN9I/vyAq4JYxXVtRDN86qq8Nv6DocazZG6CmiDtsvrRyovcN/i3ZEHKrsmjDuPFVWhLHyw3jN+DgK03WI/Rl8K9zxS2RVh3HGocZUTAAUoE5NJihtMcB+/b+zJkpmLI0Fxq7KrkSHb+cSyE4nNudeVqoipboGXdZvlQ9Gnwq2LGfGfbnBqwlg1xS5FNkl1Tg7wfLvMvou6fr5rjcv9YjT6wPnHFl++MZMRbyvcqOwqlwbpGq/QZiQ2CVhz5+PAQOM84Igk2mK1qnyzes0I9I82aX4QwTGuwxcJTc63seEXeC4NFZDvxvlPYP3IAhgwCJZrTWH9yALoH+dxbYWTmAP+Bdl+M8gOrgifBiCAVRjWj6wCyKnrYW7IAo4JY4phOmHxOEvDGE7jy+NPHo7jOOFhhaeLllu/CQKDjtGWML5ww6Mftl5O8qVhMIwNaSfGagfbKQ2cq08PRw3DvRL5gDHhUceY8KhjTHi0QfQv3WxwqZwG02wAAAAASUVORK5CYII=" + }, + "95442b2e-f15e-4def-b270-efb106facb4e": { + "name": "eWBM eFA310 FIDO2 Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "cdbdaea2-c415-5073-50f7-c04e968640b6": { + "name": "Excelsecu eSecu FIDO2 Security Key", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3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", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIwAAAAYCAYAAAAoNxVrAAAACXBIWXMAAB7CAAAewgFu0HU+AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNS0yM1QxNDo0MDo1NSswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDUtMDVUMDk6MzM6NDcrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjE4NWYyYmYtODVmOS1jZjQ3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" + }, + "bc2fe499-0d8e-4ffe-96f3-94a82840cf8c": { + "name": "OCTATCO EzQuant FIDO2 AUTHENTICATOR", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "eb3b131e-59dc-536a-d176-cb7306da10f5": { + "name": "ellipticSecure MIRkey USB Authenticator", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "1c086528-58d5-f211-823c-356786e36140": { + "name": "Atos CardOS FIDO2", + "icon_light": "data:image/png;base64,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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAKAEGACtYuHw7fWlJAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAKAEGACtYuHw7fWlJAAAAAElFTkSuQmCC" + }, + "77010bd7-212a-4fc9-b236-d2ca5e9d4084": { + "name": "Feitian BioPass FIDO2 Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC" + }, + "d94a29d9-52dd-4247-9c2d-8b818b610389": { + "name": "VeriMark Guard Fingerprint Key", + "icon_light": "data:image/png;base64,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", + "icon_dark": "data:image/png;base64,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" + }, + "833b721a-ff5f-4d00-bb2e-bdda3ec01e29": { + "name": "Feitian ePass FIDO2 Authenticator", + "icon_light": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC", + "icon_dark": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAAUCAMAAAAtBkrlAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABHZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxNi0xMi0zMFQxNDozMzowOCswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTYtMTItMzBUMDc6MzE6NTkrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6SGlzdG9yeT0iMjAxNi0xMi0zMFQxNTozMDoyNyswODowMCYjeDk75paH5Lu2IOacquagh+mimC0xIOW3suaJk+W8gCYjeEE7IiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjJFNzFCRkZDQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjJFNzFCRkZEQzY3RjExRTY5NzhEQTlDQkI2NDYzRjkwIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MkU3MUJGRkFDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkU3MUJGRkJDNjdGMTFFNjk3OERBOUNCQjY0NjNGOTAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz477JXFAAAAYFBMVEX///8EVqIXZavG2OoqcLG2zOOkwt0BSJtqlcXV4u+autlWhbzk7PUAMY9HcrKjtNbq8feAl8aBoszz9vpdjsGGqtF3n8uTsNSZpc6JsNT5+v0xYKnu8Pff5/L48fg/friczJgYAAADAElEQVR42kRUCZbDIAjFXZOY1TatNc39bzksSYc3r4ME4fMBAaD6zl8y/9TOget8d5jfN78bwM/dDCRpR521zXfojHJ05IIyhBAUSVAONdGzBYt2f7KFrfkJaAkHh9FZhcDXHRkTKo9MLihGaavImnV3qyEX0Eprgz/4DwUD7kCHRnd8QFN43Go4UVmDDgza4w27oizdA2+cK+uuUpjjo2+xwc/42W50x5LGYeDBsR0HVIx5x8iF60CblbTEEkFr27bNDBUVSq1OKVPbE62b3EH8FqBg5OOOEuc2t8ZJiqMOuGp+cKjg7wVGceozqN4pxgVPQkjFYgbVJKDUhDCjYrawP5q4ETgC9fIMRHtitpQcCvJOELcbMsQgnciRkljpyQjvG44jqBUETFiBi1PEIyekOzsW+Ty5cLHos5R+dMS1LtSSxf3gQHczR2CI4gMNpW4IRA1QMa6tJ4+C6uHuGE8mNDIyFqg/OP/MMUueS6Iq8S90dAeBJSEy/qKkK+BNwz8cYY4jb5J6u4iWCI2B1Z56LW5kEc4hkdMpsvUC5585SX0QubcgNqyfgDFEcTt+40/0S5Nx0waCw3OKkcObA5In0AYp01pjjw2n626UDjtHwa28iHuTKqtrv+reW41NZ6iGlr7uuLJCfkFtctcG04sgm1eNS+ZaDnpaTErGoyX5JK2iMz8xs0nOwWGcPDN49qaCd4bzJozDZm/aBK+EozLw+XhNBiYwHf0siOu1XPkG/zKwvqYKcfSwDEcH/oUe07es/WQ8rIyg2DOXj8tjkZduDB/b8hzDllMMOCS5BEnd534f8ti3UZc4kMs3xLyafMSsJhdG8XPqjNk5tAgO25feKChnVdDj/J0FMkOsU/xMBv0wFhYeEGfVH13fuDU0yDFLa4fc7RnWHBfuTFV2tEmNwadc7ac3UY2jfBl7HT36fe34iQO5mNCFFBW07KjPgqhOLU01vZ8PueZ2JClFZN8jkUs69uka9ePp6+EfL4AF5+NywSbirHtcB8Ml/gkwAEjkK64KjHPeAAAAAElFTkSuQmCC" + } +} diff --git a/x/webauthnx/aaguid/passkey-aaguids.json b/x/webauthnx/aaguid/passkey-aaguids.json new file mode 100644 index 000000000000..8410d2e6c5b9 --- /dev/null +++ b/x/webauthnx/aaguid/passkey-aaguids.json @@ -0,0 +1,105 @@ +{ + "ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4": { + "name": "Google Password Manager", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE5MiAxOTIiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDE5MiAxOTIiIHdpZHRoPSIyNHB4Ij48cmVjdCBmaWxsPSJub25lIiBoZWlnaHQ9IjE5MiIgd2lkdGg9IjE5MiIgeT0iMCIvPjxnPjxwYXRoIGQ9Ik02OS4yOSwxMDZjLTMuNDYsNS45Ny05LjkxLDEwLTE3LjI5LDEwYy0xMS4wMywwLTIwLTguOTctMjAtMjBzOC45Ny0yMCwyMC0yMCBjNy4zOCwwLDEzLjgzLDQuMDMsMTcuMjksMTBoMjUuNTVDOTAuMyw2Ni41NCw3Mi44Miw1Miw1Miw1MkMyNy43NCw1Miw4LDcxLjc0LDgsOTZzMTkuNzQsNDQsNDQsNDRjMjAuODIsMCwzOC4zLTE0LjU0LDQyLjg0LTM0IEg2OS4yOXoiIGZpbGw9IiM0Mjg1RjQiLz48cmVjdCBmaWxsPSIjRkJCQzA0IiBoZWlnaHQ9IjI0IiB3aWR0aD0iNDQiIHg9Ijk0IiB5PSI4NCIvPjxwYXRoIGQ9Ik05NC4zMiw4NEg2OHYwLjA1YzIuNSwzLjM0LDQsNy40Nyw0LDExLjk1cy0xLjUsOC42MS00LDExLjk1VjEwOGgyNi4zMiBjMS4wOC0zLjgyLDEuNjgtNy44NCwxLjY4LTEyUzk1LjQxLDg3LjgyLDk0LjMyLDg0eiIgZmlsbD0iI0VBNDMzNSIvPjxwYXRoIGQ9Ik0xODQsMTA2djI2aC0xNnYtOGMwLTQuNDItMy41OC04LTgtOHMtOCwzLjU4LTgsOHY4aC0xNnYtMjZIMTg0eiIgZmlsbD0iIzM0QTg1MyIvPjxyZWN0IGZpbGw9IiMxODgwMzgiIGhlaWdodD0iMjQiIHdpZHRoPSI0OCIgeD0iMTM2IiB5PSI4NCIvPjwvZz48L3N2Zz4=", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE5MiAxOTIiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDE5MiAxOTIiIHdpZHRoPSIyNHB4Ij48cmVjdCBmaWxsPSJub25lIiBoZWlnaHQ9IjE5MiIgd2lkdGg9IjE5MiIgeT0iMCIvPjxnPjxwYXRoIGQ9Ik02OS4yOSwxMDZjLTMuNDYsNS45Ny05LjkxLDEwLTE3LjI5LDEwYy0xMS4wMywwLTIwLTguOTctMjAtMjBzOC45Ny0yMCwyMC0yMCBjNy4zOCwwLDEzLjgzLDQuMDMsMTcuMjksMTBoMjUuNTVDOTAuMyw2Ni41NCw3Mi44Miw1Miw1Miw1MkMyNy43NCw1Miw4LDcxLjc0LDgsOTZzMTkuNzQsNDQsNDQsNDRjMjAuODIsMCwzOC4zLTE0LjU0LDQyLjg0LTM0IEg2OS4yOXoiIGZpbGw9IiM0Mjg1RjQiLz48cmVjdCBmaWxsPSIjRkJCQzA0IiBoZWlnaHQ9IjI0IiB3aWR0aD0iNDQiIHg9Ijk0IiB5PSI4NCIvPjxwYXRoIGQ9Ik05NC4zMiw4NEg2OHYwLjA1YzIuNSwzLjM0LDQsNy40Nyw0LDExLjk1cy0xLjUsOC42MS00LDExLjk1VjEwOGgyNi4zMiBjMS4wOC0zLjgyLDEuNjgtNy44NCwxLjY4LTEyUzk1LjQxLDg3LjgyLDk0LjMyLDg0eiIgZmlsbD0iI0VBNDMzNSIvPjxwYXRoIGQ9Ik0xODQsMTA2djI2aC0xNnYtOGMwLTQuNDItMy41OC04LTgtOHMtOCwzLjU4LTgsOHY4aC0xNnYtMjZIMTg0eiIgZmlsbD0iIzM0QTg1MyIvPjxyZWN0IGZpbGw9IiMxODgwMzgiIGhlaWdodD0iMjQiIHdpZHRoPSI0OCIgeD0iMTM2IiB5PSI4NCIvPjwvZz48L3N2Zz4=" + }, + "adce0002-35bc-c60a-648b-0b25f1f05503": { + "name": "Chrome on Mac", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDggNDgiPgogIDxkZWZzPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJhIiB4MT0iMy4yMTczIiB5MT0iMTUiIHgyPSI0NC43ODEyIiB5Mj0iMTUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZDkzMDI1Ii8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2VhNDMzNSIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjIwLjcyMTkiIHkxPSI0Ny42NzkxIiB4Mj0iNDEuNTAzOSIgeTI9IjExLjY4MzciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmNjOTM0Ii8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZiYmMwNCIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYyIgeDE9IjI2LjU5ODEiIHkxPSI0Ni41MDE1IiB4Mj0iNS44MTYxIiB5Mj0iMTAuNTA2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzFlOGUzZSIvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzNGE4NTMiLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgICAKICAgIDxwYXRoIGlkPSJwIiBkPSJNMTMuNjA4NiAzMC4wMDMxIDMuMjE4IDEyLjAwNkEyMy45OTQgMjMuOTk0IDAgMCAwIDI0LjAwMjUgNDhsMTAuMzkwNi0xNy45OTcxLS4wMDY3LS4wMDY4YTExLjk4NTIgMTEuOTg1MiAwIDAgMS0yMC43Nzc4LjAwN1oiLz4KICA8L2RlZnM+CiAgCiAgPHVzZSB4bGluazpocmVmPSIjcCIgZmlsbD0idXJsKCNhKSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDI0IDI0KSIvPgogIDx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGw9InVybCgjYikiIHRyYW5zZm9ybT0icm90YXRlKC0xMjAgMjQgMjQpIi8+CiAgPHVzZSB4bGluazpocmVmPSIjcCIgZmlsbD0idXJsKCNjKSIvPgogIAogIDxjaXJjbGUgY3g9IjI0IiBjeT0iMjQiIHI9IjEyIiBzdHlsZT0iZmlsbDojZmZmIi8+CiAgPGNpcmNsZSBjeD0iMjQiIGN5PSIyNCIgcj0iOS41IiBzdHlsZT0iZmlsbDojMWE3M2U4Ii8+Cjwvc3ZnPg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDggNDgiPgogIDxkZWZzPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJhIiB4MT0iMy4yMTczIiB5MT0iMTUiIHgyPSI0NC43ODEyIiB5Mj0iMTUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZDkzMDI1Ii8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2VhNDMzNSIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjIwLjcyMTkiIHkxPSI0Ny42NzkxIiB4Mj0iNDEuNTAzOSIgeTI9IjExLjY4MzciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmNjOTM0Ii8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZiYmMwNCIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYyIgeDE9IjI2LjU5ODEiIHkxPSI0Ni41MDE1IiB4Mj0iNS44MTYxIiB5Mj0iMTAuNTA2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzFlOGUzZSIvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzNGE4NTMiLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgICAKICAgIDxwYXRoIGlkPSJwIiBkPSJNMTMuNjA4NiAzMC4wMDMxIDMuMjE4IDEyLjAwNkEyMy45OTQgMjMuOTk0IDAgMCAwIDI0LjAwMjUgNDhsMTAuMzkwNi0xNy45OTcxLS4wMDY3LS4wMDY4YTExLjk4NTIgMTEuOTg1MiAwIDAgMS0yMC43Nzc4LjAwN1oiLz4KICA8L2RlZnM+CiAgCiAgPHVzZSB4bGluazpocmVmPSIjcCIgZmlsbD0idXJsKCNhKSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDI0IDI0KSIvPgogIDx1c2UgeGxpbms6aHJlZj0iI3AiIGZpbGw9InVybCgjYikiIHRyYW5zZm9ybT0icm90YXRlKC0xMjAgMjQgMjQpIi8+CiAgPHVzZSB4bGluazpocmVmPSIjcCIgZmlsbD0idXJsKCNjKSIvPgogIAogIDxjaXJjbGUgY3g9IjI0IiBjeT0iMjQiIHI9IjEyIiBzdHlsZT0iZmlsbDojZmZmIi8+CiAgPGNpcmNsZSBjeD0iMjQiIGN5PSIyNCIgcj0iOS41IiBzdHlsZT0iZmlsbDojMWE3M2U4Ii8+Cjwvc3ZnPg==" + }, + "08987058-cadc-4b81-b6e1-30de50dcbe96": { + "name": "Windows Hello", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMwMDc4ZDQ7c3Ryb2tlLXdpZHRoOjBweDt9PC9zdHlsZT48L2RlZnM+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIyNC4yNSIgeT0iMjQuMjUiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMTMzLjQiIHk9IjI0LjI1IiB3aWR0aD0iOTguMzUiIGhlaWdodD0iOTguMzUiLz48cmVjdCBjbGFzcz0iY2xzLTEiIHg9IjI0LjI1IiB5PSIxMzMuNCIgd2lkdGg9Ijk4LjM1IiBoZWlnaHQ9Ijk4LjM1Ii8+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIxMzMuNCIgeT0iMTMzLjQiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjwvc3ZnPg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMwMDc4ZDQ7c3Ryb2tlLXdpZHRoOjBweDt9PC9zdHlsZT48L2RlZnM+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIyNC4yNSIgeT0iMjQuMjUiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMTMzLjQiIHk9IjI0LjI1IiB3aWR0aD0iOTguMzUiIGhlaWdodD0iOTguMzUiLz48cmVjdCBjbGFzcz0iY2xzLTEiIHg9IjI0LjI1IiB5PSIxMzMuNCIgd2lkdGg9Ijk4LjM1IiBoZWlnaHQ9Ijk4LjM1Ii8+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIxMzMuNCIgeT0iMTMzLjQiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjwvc3ZnPg==" + }, + "9ddd1817-af5a-4672-a2b9-3e3dd95000a9": { + "name": "Windows Hello", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMwMDc4ZDQ7c3Ryb2tlLXdpZHRoOjBweDt9PC9zdHlsZT48L2RlZnM+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIyNC4yNSIgeT0iMjQuMjUiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMTMzLjQiIHk9IjI0LjI1IiB3aWR0aD0iOTguMzUiIGhlaWdodD0iOTguMzUiLz48cmVjdCBjbGFzcz0iY2xzLTEiIHg9IjI0LjI1IiB5PSIxMzMuNCIgd2lkdGg9Ijk4LjM1IiBoZWlnaHQ9Ijk4LjM1Ii8+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIxMzMuNCIgeT0iMTMzLjQiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjwvc3ZnPg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMwMDc4ZDQ7c3Ryb2tlLXdpZHRoOjBweDt9PC9zdHlsZT48L2RlZnM+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIyNC4yNSIgeT0iMjQuMjUiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMTMzLjQiIHk9IjI0LjI1IiB3aWR0aD0iOTguMzUiIGhlaWdodD0iOTguMzUiLz48cmVjdCBjbGFzcz0iY2xzLTEiIHg9IjI0LjI1IiB5PSIxMzMuNCIgd2lkdGg9Ijk4LjM1IiBoZWlnaHQ9Ijk4LjM1Ii8+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIxMzMuNCIgeT0iMTMzLjQiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjwvc3ZnPg==" + }, + "6028b017-b1d4-4c02-b4b3-afcdafc96bb2": { + "name": "Windows Hello", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMwMDc4ZDQ7c3Ryb2tlLXdpZHRoOjBweDt9PC9zdHlsZT48L2RlZnM+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIyNC4yNSIgeT0iMjQuMjUiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMTMzLjQiIHk9IjI0LjI1IiB3aWR0aD0iOTguMzUiIGhlaWdodD0iOTguMzUiLz48cmVjdCBjbGFzcz0iY2xzLTEiIHg9IjI0LjI1IiB5PSIxMzMuNCIgd2lkdGg9Ijk4LjM1IiBoZWlnaHQ9Ijk4LjM1Ii8+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIxMzMuNCIgeT0iMTMzLjQiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjwvc3ZnPg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMwMDc4ZDQ7c3Ryb2tlLXdpZHRoOjBweDt9PC9zdHlsZT48L2RlZnM+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIyNC4yNSIgeT0iMjQuMjUiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMTMzLjQiIHk9IjI0LjI1IiB3aWR0aD0iOTguMzUiIGhlaWdodD0iOTguMzUiLz48cmVjdCBjbGFzcz0iY2xzLTEiIHg9IjI0LjI1IiB5PSIxMzMuNCIgd2lkdGg9Ijk4LjM1IiBoZWlnaHQ9Ijk4LjM1Ii8+PHJlY3QgY2xhc3M9ImNscy0xIiB4PSIxMzMuNCIgeT0iMTMzLjQiIHdpZHRoPSI5OC4zNSIgaGVpZ2h0PSI5OC4zNSIvPjwvc3ZnPg==" + }, + "dd4ec289-e01d-41c9-bb89-70fa845d4bf2": { + "name": "iCloud Keychain (Managed)", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJub25lIj48cGF0aCBkPSJtMjE3LjM2LDkwLjY5Yy0xNS41OCw5LjU0LTI1LjE3LDI2LjQxLTI1LjM4LDQ0LjY4LjA2LDIwLjY3LDEyLjQzLDM5LjMyLDMxLjQ2LDQ3LjQxLTMuNjcsMTEuODQtOS4xLDIzLjA2LTE2LjExLDMzLjI4LTEwLjAzLDE0LjQ0LTIwLjUyLDI4Ljg3LTM2LjQ3LDI4Ljg3cy0yMC4wNi05LjI3LTM4LjQ1LTkuMjctMjQuMzIsOS41Ny0zOC45LDkuNTctMjQuNzctMTMuMzctMzYuNDctMjkuNzljLTE1LjQ2LTIyLjk5LTIzLjk1LTQ5Ljk2LTI0LjQ3LTc3LjY2LDAtNDUuNTksMjkuNjMtNjkuNzUsNTguODEtNjkuNzUsMTUuNSwwLDI4LjQyLDEwLjE4LDM4LjE1LDEwLjE4czIzLjcxLTEwLjc5LDQxLjM0LTEwLjc5YzE4LjQxLS40NywzNS44NCw4LjI0LDQ2LjUsMjMuMjVabS01NC44Ni00Mi41NWM3Ljc3LTkuMTQsMTIuMTctMjAuNjcsMTIuNDYtMzIuNjcuMDEtMS41OC0uMTQtMy4xNi0uNDYtNC43MS0xMy4zNSwxLjMtMjUuNjksNy42Ny0zNC41LDE3Ljc4LTcuODUsOC43OC0xMi40MSwyMC0xMi45MiwzMS43NiwwLDEuNDMuMTYsMi44Ni40Niw0LjI2LDEuMDUuMiwyLjEyLjMsMy4xOS4zLDEyLjQzLS45OSwyMy45MS03LjA0LDMxLjc2LTE2LjczWiIgZmlsbD0iI0ZGRiIvPjwvc3ZnPg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJub25lIj48cGF0aCBkPSJtMjE3LjM2LDkwLjY5Yy0xNS41OCw5LjU0LTI1LjE3LDI2LjQxLTI1LjM4LDQ0LjY4LjA2LDIwLjY3LDEyLjQzLDM5LjMyLDMxLjQ2LDQ3LjQxLTMuNjcsMTEuODQtOS4xLDIzLjA2LTE2LjExLDMzLjI4LTEwLjAzLDE0LjQ0LTIwLjUyLDI4Ljg3LTM2LjQ3LDI4Ljg3cy0yMC4wNi05LjI3LTM4LjQ1LTkuMjctMjQuMzIsOS41Ny0zOC45LDkuNTctMjQuNzctMTMuMzctMzYuNDctMjkuNzljLTE1LjQ2LTIyLjk5LTIzLjk1LTQ5Ljk2LTI0LjQ3LTc3LjY2LDAtNDUuNTksMjkuNjMtNjkuNzUsNTguODEtNjkuNzUsMTUuNSwwLDI4LjQyLDEwLjE4LDM4LjE1LDEwLjE4czIzLjcxLTEwLjc5LDQxLjM0LTEwLjc5YzE4LjQxLS40NywzNS44NCw4LjI0LDQ2LjUsMjMuMjVabS01NC44Ni00Mi41NWM3Ljc3LTkuMTQsMTIuMTctMjAuNjcsMTIuNDYtMzIuNjcuMDEtMS41OC0uMTQtMy4xNi0uNDYtNC43MS0xMy4zNSwxLjMtMjUuNjksNy42Ny0zNC41LDE3Ljc4LTcuODUsOC43OC0xMi40MSwyMC0xMi45MiwzMS43NiwwLDEuNDMuMTYsMi44Ni40Niw0LjI2LDEuMDUuMiwyLjEyLjMsMy4xOS4zLDEyLjQzLS45OSwyMy45MS03LjA0LDMxLjc2LTE2LjczWiIgZmlsbD0iIzAwMCIvPjwvc3ZnPg==" + }, + "531126d6-e717-415c-9320-3d9aa6981239": { + "name": "Dashlane", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yNTcuNDc0IDM1OS4yMDlDMjU3LjQ3NCAzNTYuMTg5IDI1NC40NTQgMzUzLjE2OSAyNTAuMjE1IDM1MS45NTlMMTk5LjQxMSAzMzMuMjMxQzE5MC44OTUgMzI5LjYwMSAxODEuMjY0IDMzMy44MzEgMTgxLjI2NCAzMzkuODlWNDc1Ljc3OUMxODEuMjY0IDQ3OC44MDkgMTg0LjI4MyA0ODIuNDM4IDE4Ny4zMDMgNDgzLjY0OEwyMzkuMzI2IDUwMi4zNzZDMjQ3LjE5NSA1MDUuMzk2IDI1Ny40NzQgNTAxLjE2NiAyNTcuNDc0IDQ5NC41MDhWMzU5LjIwOVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0zNTIuNzM2IDMyMS4xMDRDMzUyLjczNiAzMTguMDg0IDM0OS43MTcgMzE1LjA2NCAzNDUuNDc3IDMxMy44NTRMMjk0LjY3NCAyOTUuMTI2QzI4Ni4xNTcgMjkxLjQ5NiAyNzYuNTI2IDI5NS43MjYgMjc2LjUyNiAzMDEuNzg1VjQzNy42NzRDMjc2LjUyNiA0NDAuNzA0IDI3OS41NDYgNDQ0LjMzMyAyODIuNTY2IDQ0NS41NDNMMzM0LjU4OSA0NjQuMjcxQzM0Mi40NTggNDY3LjI5MSAzNTIuNzM2IDQ2My4wNjEgMzUyLjczNiA0NTYuNDAzVjMyMS4xMDRaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjU3LjQ3NCAzNS4zMjExQzI1Ny40NzQgMzIuMzAxMyAyNTQuNDU0IDI5LjI4MTUgMjUwLjIxNSAyOC4wNzE3TDE5OS40MTEgOS4zNDM0M0MxOTAuODk1IDUuNzEzOTkgMTgxLjI2NCA5Ljk0MzU4IDE4MS4yNjQgMTYuMDAyMlYxNTEuODkyQzE4MS4yNjQgMTU0LjkyMSAxODQuMjgzIDE1OC41NTEgMTg3LjMwMyAxNTkuNzZMMjM5LjMyNiAxNzguNDg5QzI0Ny4xOTUgMTgxLjUwOSAyNTcuNDc0IDE3Ny4yNzkgMjU3LjQ3NCAxNzAuNjJWMzUuMzIxMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0zNTIuNzM2IDkyLjQ3NzdDMzUyLjczNiA4OS40NTc5IDM0OS43MTcgODYuNDM4MiAzNDUuNDc3IDg1LjIyODNMMjk0LjY3NCA2Ni41QzI4Ni4xNTcgNjIuODcwNiAyNzYuNTI2IDY3LjEwMDIgMjc2LjUyNiA3My4xNTg4VjIwOS4wNDhDMjc2LjUyNiAyMTIuMDc4IDI3OS41NDYgMjE1LjcwNyAyODIuNTY2IDIxNi45MTdMMzM0LjU4OSAyMzUuNjQ1QzM0Mi40NTggMjM4LjY2NSAzNTIuNzM2IDIzNC40MzYgMzUyLjczNiAyMjcuNzc3VjkyLjQ3NzdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNDQ4IDE2OC42ODdDNDQ4IDE2NS42NjcgNDQ0Ljk4IDE2Mi42NDcgNDQwLjc0MSAxNjEuNDM3TDM4OS45MzcgMTQyLjcwOUMzODEuNDIxIDEzOS4wNzkgMzcxLjc5IDE0My4zMDkgMzcxLjc5IDE0OS4zNjhWMzYxLjQ2NkMzNzEuNzkgMzY0LjQ5NSAzNzQuODEgMzY4LjEyNSAzNzcuODI5IDM2OS4zMzVMNDI5Ljg1MiAzODguMDYzQzQzNy43MjEgMzkxLjA4MyA0NDggMzg2Ljg1MyA0NDggMzgwLjE5NFYxNjguNjg3WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTE2Mi4yMSAzNS4zMzA2QzE2Mi4yMSAzMi4zMTA4IDE1OS4xOSAyOS4yODE1IDE1NC45NTEgMjguMDcxN0wxMDQuMTQ4IDkuMzQzNDNDOTUuNjc4NyA1LjcxMzk5IDg2IDkuOTQzNTggODYgMTYuMDAyMlY0NzUuNzg5Qzg2IDQ3OC44MDggODkuMDE5OCA0ODIuNDM4IDkyLjA0OTIgNDgzLjY0OEwxNDQuMDYzIDUwMi4zNzZDMTUxLjkzMSA1MDUuMzk2IDE2Mi4yMSA1MDEuMTY2IDE2Mi4yMSA0OTQuNTA3VjM1LjMzMDZaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yNTcuNDc0IDM1OS4yMDlDMjU3LjQ3NCAzNTYuMTg5IDI1NC40NTQgMzUzLjE2OSAyNTAuMjE1IDM1MS45NTlMMTk5LjQxMSAzMzMuMjMxQzE5MC44OTUgMzI5LjYwMSAxODEuMjY0IDMzMy44MzEgMTgxLjI2NCAzMzkuODlWNDc1Ljc3OUMxODEuMjY0IDQ3OC44MDkgMTg0LjI4MyA0ODIuNDM4IDE4Ny4zMDMgNDgzLjY0OEwyMzkuMzI2IDUwMi4zNzZDMjQ3LjE5NSA1MDUuMzk2IDI1Ny40NzQgNTAxLjE2NiAyNTcuNDc0IDQ5NC41MDhWMzU5LjIwOVoiIGZpbGw9IiMwOTM2M0YiLz4KPHBhdGggZD0iTTM1Mi43MzYgMzIxLjEwM0MzNTIuNzM2IDMxOC4wODQgMzQ5LjcxNyAzMTUuMDY0IDM0NS40NzcgMzEzLjg1NEwyOTQuNjc0IDI5NS4xMjZDMjg2LjE1NyAyOTEuNDk2IDI3Ni41MjYgMjk1LjcyNiAyNzYuNTI2IDMwMS43ODVWNDM3LjY3NEMyNzYuNTI2IDQ0MC43MDQgMjc5LjU0NiA0NDQuMzMzIDI4Mi41NjYgNDQ1LjU0M0wzMzQuNTg5IDQ2NC4yNzFDMzQyLjQ1OCA0NjcuMjkxIDM1Mi43MzYgNDYzLjA2MSAzNTIuNzM2IDQ1Ni40MDNWMzIxLjEwM1oiIGZpbGw9IiMwOTM2M0YiLz4KPHBhdGggZD0iTTI1Ny40NzQgMzUuMzIxMUMyNTcuNDc0IDMyLjMwMTMgMjU0LjQ1NCAyOS4yODE1IDI1MC4yMTUgMjguMDcxN0wxOTkuNDExIDkuMzQzNDNDMTkwLjg5NSA1LjcxMzk5IDE4MS4yNjQgOS45NDM1OCAxODEuMjY0IDE2LjAwMjJWMTUxLjg5MkMxODEuMjY0IDE1NC45MjEgMTg0LjI4MyAxNTguNTUxIDE4Ny4zMDMgMTU5Ljc2TDIzOS4zMjYgMTc4LjQ4OUMyNDcuMTk1IDE4MS41MDggMjU3LjQ3NCAxNzcuMjc5IDI1Ny40NzQgMTcwLjYyVjM1LjMyMTFaIiBmaWxsPSIjMDkzNjNGIi8+CjxwYXRoIGQ9Ik0zNTIuNzM2IDkyLjQ3NzdDMzUyLjczNiA4OS40NTc5IDM0OS43MTcgODYuNDM4MiAzNDUuNDc3IDg1LjIyODNMMjk0LjY3NCA2Ni41QzI4Ni4xNTcgNjIuODcwNiAyNzYuNTI2IDY3LjEwMDIgMjc2LjUyNiA3My4xNTg4VjIwOS4wNDhDMjc2LjUyNiAyMTIuMDc4IDI3OS41NDYgMjE1LjcwNyAyODIuNTY2IDIxNi45MTdMMzM0LjU4OSAyMzUuNjQ1QzM0Mi40NTggMjM4LjY2NSAzNTIuNzM2IDIzNC40MzYgMzUyLjczNiAyMjcuNzc3VjkyLjQ3NzdaIiBmaWxsPSIjMDkzNjNGIi8+CjxwYXRoIGQ9Ik00NDggMTY4LjY4N0M0NDggMTY1LjY2NyA0NDQuOTggMTYyLjY0NyA0NDAuNzQxIDE2MS40MzdMMzg5LjkzNyAxNDIuNzA5QzM4MS40MjEgMTM5LjA3OSAzNzEuNzkgMTQzLjMwOSAzNzEuNzkgMTQ5LjM2OFYzNjEuNDY2QzM3MS43OSAzNjQuNDk1IDM3NC44MSAzNjguMTI1IDM3Ny44MjkgMzY5LjMzNUw0MjkuODUyIDM4OC4wNjNDNDM3LjcyMSAzOTEuMDgzIDQ0OCAzODYuODUzIDQ0OCAzODAuMTk0VjE2OC42ODdaIiBmaWxsPSIjMDkzNjNGIi8+CjxwYXRoIGQ9Ik0xNjIuMjEgMzUuMzMwNkMxNjIuMjEgMzIuMzEwOCAxNTkuMTkgMjkuMjgxNSAxNTQuOTUxIDI4LjA3MTdMMTA0LjE0OCA5LjM0MzQzQzk1LjY3ODcgNS43MTM5OSA4NiA5Ljk0MzU4IDg2IDE2LjAwMjJWNDc1Ljc4OUM4NiA0NzguODA4IDg5LjAxOTggNDgyLjQzOCA5Mi4wNDkyIDQ4My42NDhMMTQ0LjA2MyA1MDIuMzc2QzE1MS45MzEgNTA1LjM5NiAxNjIuMjEgNTAxLjE2NiAxNjIuMjEgNDk0LjUwN1YzNS4zMzA2WiIgZmlsbD0iIzA5MzYzRiIvPgo8L3N2Zz4K" + }, + "bada5566-a7aa-401f-bd96-45619a55120d": { + "name": "1Password", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQwIiBoZWlnaHQ9IjI0MCIgdmlld0JveD0iMCAwIDI0MCAyNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMjM5LjI1NyAxMjAuNDE3QzIzOS4yNTcgNTQuNDE5MyAxODUuNzU1IDAuOTE2NTA0IDExOS43NTcgMC45MTY1MDRDNTMuNzYwMSAwLjkxNjUwNCAwLjI1NzMyNCA1NC40MTkzIDAuMjU3MzI0IDEyMC40MTdDMC4yNTczMjQgMTg2LjQxNyA1My43NjAxIDIzOS45MTcgMTE5Ljc1NyAyMzkuOTE3QzE4NS43NTUgMjM5LjkxNyAyMzkuMjU3IDE4Ni40MTcgMjM5LjI1NyAxMjAuNDE3Wk05OC4wMDY5IDU0LjAyNzZDOTcuMDY3NCA1NS44NzE0IDk3LjA2NzQgNTguMjg1MSA5Ny4wNjc0IDYzLjExMjZWOTAuNDcyOUM5Ny4wNjc0IDkxLjY3ODggOTcuMDY3NCA5Mi4yODE3IDk3LjIxOTYgOTIuODM5MkM5Ny4zNTQ1IDkzLjMzMzEgOTcuNTc2MyA5My43OTkgOTcuODc0NiA5NC4yMTVDOTguMjExMyA5NC42ODQ3IDk4LjY3OTIgOTUuMDY0OCA5OS42MTUyIDk1LjgyNTFMMTA2LjUzNiAxMDEuNDQ3QzEwNy42NjQgMTAyLjM2NCAxMDguMjI4IDEwMi44MjIgMTA4LjQzMyAxMDMuMzc0QzEwOC42MTMgMTAzLjg1NyAxMDguNjEzIDEwNC4zOSAxMDguNDMzIDEwNC44NzNDMTA4LjIyOCAxMDUuNDI1IDEwNy42NjQgMTA1Ljg4MyAxMDYuNTM2IDEwNi44TDk5LjYxNTIgMTEyLjQyMkM5OC42NzkzIDExMy4xODIgOTguMjExMyAxMTMuNTYyIDk3Ljg3NDYgMTE0LjAzMkM5Ny41NzYzIDExNC40NDggOTcuMzU0NSAxMTQuOTE0IDk3LjIxOTYgMTE1LjQwOEM5Ny4wNjc0IDExNS45NjUgOTcuMDY3NCAxMTYuNTY4IDk3LjA2NzQgMTE3Ljc3NFYxNzcuNzE5Qzk3LjA2NzQgMTgyLjU0NyA5Ny4wNjc0IDE4NC45NjEgOTguMDA2OSAxODYuODA1Qzk4LjgzMzMgMTg4LjQyNiAxMDAuMTUyIDE4OS43NDUgMTAxLjc3NCAxOTAuNTcxQzEwMy42MTggMTkxLjUxMSAxMDYuMDMxIDE5MS41MTEgMTEwLjg1OSAxOTEuNTExSDEyOC42NTZDMTMzLjQ4MyAxOTEuNTExIDEzNS44OTcgMTkxLjUxMSAxMzcuNzQxIDE5MC41NzFDMTM5LjM2MyAxODkuNzQ1IDE0MC42ODEgMTg4LjQyNiAxNDEuNTA4IDE4Ni44MDVDMTQyLjQ0NyAxODQuOTYxIDE0Mi40NDcgMTgyLjU0NyAxNDIuNDQ3IDE3Ny43MTlWMTUwLjM1OUMxNDIuNDQ3IDE0OS4xNTMgMTQyLjQ0NyAxNDguNTUgMTQyLjI5NSAxNDcuOTkzQzE0Mi4xNiAxNDcuNDk5IDE0MS45MzggMTQ3LjAzMyAxNDEuNjQgMTQ2LjYxN0MxNDEuMzAzIDE0Ni4xNDcgMTQwLjgzNSAxNDUuNzY3IDEzOS44OTkgMTQ1LjAwN0wxMzIuOTc4IDEzOS4zODVDMTMxLjg1IDEzOC40NjggMTMxLjI4NiAxMzguMDEgMTMxLjA4MiAxMzcuNDU5QzEzMC45MDIgMTM2Ljk3NSAxMzAuOTAyIDEzNi40NDMgMTMxLjA4MiAxMzUuOTU5QzEzMS4yODYgMTM1LjQwNyAxMzEuODUgMTM0Ljk0OSAxMzIuOTc4IDEzNC4wMzNMMTM5Ljg5OSAxMjguNDFDMTQwLjgzNSAxMjcuNjUgMTQxLjMwMyAxMjcuMjcgMTQxLjY0IDEyNi44QzE0MS45MzggMTI2LjM4NCAxNDIuMTYgMTI1LjkxOCAxNDIuMjk1IDEyNS40MjRDMTQyLjQ0NyAxMjQuODY3IDE0Mi40NDcgMTI0LjI2NCAxNDIuNDQ3IDEyMy4wNThWNjMuMTEyNkMxNDIuNDQ3IDU4LjI4NTEgMTQyLjQ0NyA1NS44NzE0IDE0MS41MDggNTQuMDI3NkMxNDAuNjgxIDUyLjQwNTcgMTM5LjM2MyA1MS4wODcgMTM3Ljc0MSA1MC4yNjA2QzEzNS44OTcgNDkuMzIxMSAxMzMuNDgzIDQ5LjMyMTEgMTI4LjY1NiA0OS4zMjExSDExMC44NTlDMTA2LjAzMSA0OS4zMjExIDEwMy42MTggNDkuMzIxMSAxMDEuNzc0IDUwLjI2MDZDMTAwLjE1MiA1MS4wODcgOTguODMzMyA1Mi40MDU3IDk4LjAwNjkgNTQuMDI3NloiIGZpbGw9IiNGRkZFRkIiLz4KPC9zdmc+Cg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQwIiBoZWlnaHQ9IjI0MCIgdmlld0JveD0iMCAwIDI0MCAyNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMjM5LjExNiAxMjAuNDE3QzIzOS4xMTYgNTQuNDE5MyAxODUuNjEzIDAuOTE2NTA0IDExOS42MTYgMC45MTY1MDRDNTMuNjE5IDAuOTE2NTA0IDAuMTE2MjExIDU0LjQxOTMgMC4xMTYyMTEgMTIwLjQxN0MwLjExNjIxMSAxODYuNDE3IDUzLjYxOSAyMzkuOTE3IDExOS42MTYgMjM5LjkxN0MxODUuNjEzIDIzOS45MTcgMjM5LjExNiAxODYuNDE3IDIzOS4xMTYgMTIwLjQxN1pNOTcuODY1OCA1NC4wMjc2Qzk2LjkyNjMgNTUuODcxNCA5Ni45MjYzIDU4LjI4NTEgOTYuOTI2MyA2My4xMTI2VjkwLjQ3MjlDOTYuOTI2MyA5MS42Nzg4IDk2LjkyNjMgOTIuMjgxNyA5Ny4wNzg1IDkyLjgzOTJDOTcuMjEzNCA5My4zMzMxIDk3LjQzNTIgOTMuNzk5IDk3LjczMzUgOTQuMjE1Qzk4LjA3MDIgOTQuNjg0NyA5OC41MzgxIDk1LjA2NDggOTkuNDc0MSA5NS44MjUxTDEwNi4zOTUgMTAxLjQ0N0MxMDcuNTIzIDEwMi4zNjQgMTA4LjA4NyAxMDIuODIyIDEwOC4yOTIgMTAzLjM3NEMxMDguNDcxIDEwMy44NTcgMTA4LjQ3MSAxMDQuMzkgMTA4LjI5MiAxMDQuODczQzEwOC4wODcgMTA1LjQyNSAxMDcuNTIzIDEwNS44ODMgMTA2LjM5NSAxMDYuOEw5OS40NzQxIDExMi40MjJDOTguNTM4MiAxMTMuMTgyIDk4LjA3MDIgMTEzLjU2MiA5Ny43MzM1IDExNC4wMzJDOTcuNDM1MiAxMTQuNDQ4IDk3LjIxMzQgMTE0LjkxNCA5Ny4wNzg1IDExNS40MDhDOTYuOTI2MyAxMTUuOTY1IDk2LjkyNjMgMTE2LjU2OCA5Ni45MjYzIDExNy43NzRWMTc3LjcxOUM5Ni45MjYzIDE4Mi41NDcgOTYuOTI2MyAxODQuOTYxIDk3Ljg2NTggMTg2LjgwNUM5OC42OTIyIDE4OC40MjYgMTAwLjAxMSAxODkuNzQ1IDEwMS42MzMgMTkwLjU3MUMxMDMuNDc3IDE5MS41MTEgMTA1Ljg5IDE5MS41MTEgMTEwLjcxOCAxOTEuNTExSDEyOC41MTVDMTMzLjM0MiAxOTEuNTExIDEzNS43NTYgMTkxLjUxMSAxMzcuNiAxOTAuNTcxQzEzOS4yMjEgMTg5Ljc0NSAxNDAuNTQgMTg4LjQyNiAxNDEuMzY3IDE4Ni44MDVDMTQyLjMwNiAxODQuOTYxIDE0Mi4zMDYgMTgyLjU0NyAxNDIuMzA2IDE3Ny43MTlWMTUwLjM1OUMxNDIuMzA2IDE0OS4xNTMgMTQyLjMwNiAxNDguNTUgMTQyLjE1NCAxNDcuOTkzQzE0Mi4wMTkgMTQ3LjQ5OSAxNDEuNzk3IDE0Ny4wMzMgMTQxLjQ5OSAxNDYuNjE3QzE0MS4xNjIgMTQ2LjE0NyAxNDAuNjk0IDE0NS43NjcgMTM5Ljc1OCAxNDUuMDA3TDEzMi44MzcgMTM5LjM4NUMxMzEuNzA5IDEzOC40NjggMTMxLjE0NSAxMzguMDEgMTMwLjk0IDEzNy40NTlDMTMwLjc2MSAxMzYuOTc1IDEzMC43NjEgMTM2LjQ0MyAxMzAuOTQgMTM1Ljk1OUMxMzEuMTQ1IDEzNS40MDcgMTMxLjcwOSAxMzQuOTQ5IDEzMi44MzcgMTM0LjAzM0wxMzkuNzU4IDEyOC40MUMxNDAuNjk0IDEyNy42NSAxNDEuMTYyIDEyNy4yNyAxNDEuNDk5IDEyNi44QzE0MS43OTcgMTI2LjM4NCAxNDIuMDE5IDEyNS45MTggMTQyLjE1NCAxMjUuNDI0QzE0Mi4zMDYgMTI0Ljg2NyAxNDIuMzA2IDEyNC4yNjQgMTQyLjMwNiAxMjMuMDU4VjYzLjExMjZDMTQyLjMwNiA1OC4yODUxIDE0Mi4zMDYgNTUuODcxNCAxNDEuMzY3IDU0LjAyNzZDMTQwLjU0IDUyLjQwNTcgMTM5LjIyMSA1MS4wODcgMTM3LjYgNTAuMjYwNkMxMzUuNzU2IDQ5LjMyMTEgMTMzLjM0MiA0OS4zMjExIDEyOC41MTUgNDkuMzIxMUgxMTAuNzE4QzEwNS44OSA0OS4zMjExIDEwMy40NzcgNDkuMzIxMSAxMDEuNjMzIDUwLjI2MDZDMTAwLjAxMSA1MS4wODcgOTguNjkyMiA1Mi40MDU3IDk3Ljg2NTggNTQuMDI3NloiIGZpbGw9IiMxQTI4NUYiLz4KPC9zdmc+Cg==" + }, + "b84e4048-15dc-4dd0-8640-f4f60813c8af": { + "name": "NordPass", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgODAgODAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik03LjYxMzQgNzBDMi44MjQzNSA2My4zNTIgMCA1NS4xNzIyIDAgNDYuMzI3M0MwIDI0LjA1NTIgMTcuOTA4NiA2IDQwIDZDNjIuMDkxNCA2IDgwIDI0LjA1NTIgODAgNDYuMzI3M0M4MCA1NS4xNzIxIDc3LjE3NTcgNjMuMzUxOCA3Mi4zODY3IDY5Ljk5OTlMNTMuMTc0NyAzOC41NDY2TDUxLjMxOTUgNDEuNzA0Nkw1My4yMDE4IDUwLjQ4NzdMNDAgMjcuNzE0N0wzMS44MzM0IDQxLjYxNjFMMzMuNzM0NiA1MC40ODc3TDI2LjgxNDcgMzguNTY0Nkw3LjYxMzQgNzBaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgODAgODAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik03LjYxMzQgNzBDMi44MjQzNSA2My4zNTIgMCA1NS4xNzIyIDAgNDYuMzI3M0MwIDI0LjA1NTIgMTcuOTA4NiA2IDQwIDZDNjIuMDkxNCA2IDgwIDI0LjA1NTIgODAgNDYuMzI3M0M4MCA1NS4xNzIxIDc3LjE3NTcgNjMuMzUxOCA3Mi4zODY3IDY5Ljk5OTlMNTMuMTc0NyAzOC41NDY2TDUxLjMxOTUgNDEuNzA0Nkw1My4yMDE4IDUwLjQ4NzdMNDAgMjcuNzE0N0wzMS44MzM0IDQxLjYxNjFMMzMuNzM0NiA1MC40ODc3TDI2LjgxNDcgMzguNTY0Nkw3LjYxMzQgNzBaIiBmaWxsPSIjMENBQUFCIi8+Cjwvc3ZnPgo=" + }, + "0ea242b4-43c4-4a1b-8b17-dd6d0b6baec6": { + "name": "Keeper", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzYwMzRfMzM2MjcpIj4KPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTIiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMiAxMkMyMiAxNy41MjI4IDE3LjUyMjggMjIgMTIgMjJDNi40NzcxNSAyMiAyIDE3LjUyMjggMiAxMkMyIDYuNDc3MTUgNi40NzcxNSAyIDEyIDJDMTcuNTIyOCAyIDIyIDYuNDc3MTUgMjIgMTJaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMTAuMTIxOCAzLjI3MzI1SDExLjY2NjZWOS41MTUyN0gxNC44NTc1TDE4LjY5NiA2LjQ2MzE3TDE5LjY2MDcgNy42NjgyMUwxNS4zOTg5IDExLjA1NjRIMTAuMTIxOFYzLjI3MzI1WiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNMTMuMTQzOCAzLjQ4MzY2TDE0LjY4ODcgMy44NzY5NFY2LjAzNDkyTDE2LjQxNzMgNC42MTgxMUwxNy43MDA4IDUuNTYwOTdMMTQuNDA3IDguMjYwMTNMMTMuMTQzOCA4LjI1MzQxVjMuNDgzNjZaIiBmaWxsPSIjRkZDNzAwIi8+CjxwYXRoIGQ9Ik00LjAzODcgMTUuMDg0OUw1LjU4MzU0IDE2LjM5NThWNy44MTQyN0w0LjAzODcgOS4yMjc3MlYxNS4wODQ5WiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNOC42MTI1NyAxOC4yNDExTDcuMDY2MDQgMTkuNTgwNlY0LjQ5NDg1TDguNjEyNTcgNS44MzQzNFYxOC4yNDExWiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNMTQuNjg4NyAxOC4xMTc0TDE2LjQxNzMgMTkuNTM0MkwxNy43MDA4IDE4LjU4OTdMMTQuNDA3IDE1Ljg5MjJMMTMuMTQzOCAxNS44OTg5VjIwLjY2ODdMMTQuNjg4NyAyMC4yNzU0VjE4LjExNzRaIiBmaWxsPSIjRkZDNzAwIi8+CjxwYXRoIGQ9Ik0xOC42OTYgMTcuNDc4NkwxNC44NTc1IDE0LjQyNDhIMTEuNjY2NlYyMC42NjY4SDEwLjEyMThWMTIuODg1M0gxNS4zOTg5TDE5LjY2MDcgMTYuMjczNUwxOC42OTYgMTcuNDc4NloiIGZpbGw9IiNGRkM3MDAiLz4KPHBhdGggZD0iTTE2LjczNzYgMTEuOTcwNkwxOS44OTgxIDE0LjU3MDZMMjAuODgzIDEzLjM4MjNMMTkuMTY2MSAxMS45NzA2TDIwLjg4MyAxMC41NTg4TDE5Ljg5ODEgOS4zNzA1NkwxNi43Mzc2IDExLjk3MDZaIiBmaWxsPSIjRkZDNzAwIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNjAzNF8zMzYyNyI+CjxyZWN0IHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzYwMzRfMzM2MjcpIj4KPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTIiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMiAxMkMyMiAxNy41MjI4IDE3LjUyMjggMjIgMTIgMjJDNi40NzcxNSAyMiAyIDE3LjUyMjggMiAxMkMyIDYuNDc3MTUgNi40NzcxNSAyIDEyIDJDMTcuNTIyOCAyIDIyIDYuNDc3MTUgMjIgMTJaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMTAuMTIxOCAzLjI3MzI1SDExLjY2NjZWOS41MTUyN0gxNC44NTc1TDE4LjY5NiA2LjQ2MzE3TDE5LjY2MDcgNy42NjgyMUwxNS4zOTg5IDExLjA1NjRIMTAuMTIxOFYzLjI3MzI1WiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNMTMuMTQzOCAzLjQ4MzY2TDE0LjY4ODcgMy44NzY5NFY2LjAzNDkyTDE2LjQxNzMgNC42MTgxMUwxNy43MDA4IDUuNTYwOTdMMTQuNDA3IDguMjYwMTNMMTMuMTQzOCA4LjI1MzQxVjMuNDgzNjZaIiBmaWxsPSIjRkZDNzAwIi8+CjxwYXRoIGQ9Ik00LjAzODcgMTUuMDg0OUw1LjU4MzU0IDE2LjM5NThWNy44MTQyN0w0LjAzODcgOS4yMjc3MlYxNS4wODQ5WiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNOC42MTI1NyAxOC4yNDExTDcuMDY2MDQgMTkuNTgwNlY0LjQ5NDg1TDguNjEyNTcgNS44MzQzNFYxOC4yNDExWiIgZmlsbD0iI0ZGQzcwMCIvPgo8cGF0aCBkPSJNMTQuNjg4NyAxOC4xMTc0TDE2LjQxNzMgMTkuNTM0MkwxNy43MDA4IDE4LjU4OTdMMTQuNDA3IDE1Ljg5MjJMMTMuMTQzOCAxNS44OTg5VjIwLjY2ODdMMTQuNjg4NyAyMC4yNzU0VjE4LjExNzRaIiBmaWxsPSIjRkZDNzAwIi8+CjxwYXRoIGQ9Ik0xOC42OTYgMTcuNDc4NkwxNC44NTc1IDE0LjQyNDhIMTEuNjY2NlYyMC42NjY4SDEwLjEyMThWMTIuODg1M0gxNS4zOTg5TDE5LjY2MDcgMTYuMjczNUwxOC42OTYgMTcuNDc4NloiIGZpbGw9IiNGRkM3MDAiLz4KPHBhdGggZD0iTTE2LjczNzYgMTEuOTcwNkwxOS44OTgxIDE0LjU3MDZMMjAuODgzIDEzLjM4MjNMMTkuMTY2MSAxMS45NzA2TDIwLjg4MyAxMC41NTg4TDE5Ljg5ODEgOS4zNzA1NkwxNi43Mzc2IDExLjk3MDZaIiBmaWxsPSIjRkZDNzAwIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNjAzNF8zMzYyNyI+CjxyZWN0IHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K" + }, + "f3809540-7f14-49c1-a8b3-8f813b225541": { + "name": "Enpass", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yNTYuNDgzIDI4LjA1NTRDMzEzLjg5OSAyOC4wNTU0IDM3MS4zMTUgMjcuODg1NiA0MjguNzQ1IDI4LjE0MDVDNDQwLjY4IDI3LjkwNzYgNDUyLjUyMSAzMC4yODg5IDQ2My40NDEgMzUuMTE3OUM0NzQuMzYyIDM5Ljk0NjkgNDg0LjA5OSA0Ny4xMDczIDQ5MS45NzEgNTYuMDk4NEM1MDQuMDYyIDY5LjY5NjIgNTExLjEzMiA4Ny4wMzg3IDUxMiAxMDUuMjNDNTEyLjAyOCAxMjEuOTMzIDUxMC4wMzUgMTM4LjU3OCA1MDYuMDYzIDE1NC44MDFDNDk4LjQ0NCAxOTguNzA2IDQ5MC41MTUgMjQyLjUyNyA0ODIuNzI2IDI4Ni4zNzZDNDc2LjAxMiAzMjQuMTM0IDQ2OS41ODEgMzYxLjk1IDQ2Mi41MTMgMzk5LjY4QzQ1Ny42NzIgNDIwLjk2OSA0NDYuNTQ1IDQ0MC4zMDMgNDMwLjU4IDQ1NS4xNjVDNDE0LjYxNiA0NzAuMDI3IDM5NC41NTUgNDc5LjcyNyAzNzMuMDExIDQ4My4wMDJDMzY3Ljc1MiA0ODMuNjI5IDM2Mi40NjIgNDgzLjk0NiAzNTcuMTY2IDQ4My45NUMyOTAuMDUzIDQ4NC4wMTcgMjIyLjk0IDQ4NC4wMTcgMTU1LjgyOCA0ODMuOTVDMTMwLjQ2NiA0ODMuOSAxMDUuOTMgNDc0LjkxNSA4Ni41MTMyIDQ1OC41NjZDNjcuMDk2NSA0NDIuMjE4IDU0LjAzNjIgNDE5LjU0OCA0OS42MTggMzk0LjUyNUMzNi4xODA0IDMxOS4xNzcgMjIuNjI5NyAyNDMuODUzIDguOTY1OTcgMTY4LjU1M0M2LjI4MDM0IDE1My42MzkgMy4zMTIgMTM4LjgxMSAxLjIwNTkgMTIzLjc4NEMtMi40NjEwNSAxMDIuNzI5IDIuMzEwOTMgODEuMDc0NCAxNC40ODUyIDYzLjUyNDRDMjYuNjU5NiA0NS45NzQ1IDQ1LjI1MjkgMzMuOTQ2MiA2Ni4yMjY2IDMwLjA1MjVDNzMuMDU1NyAyOC43NDUxIDc5Ljk5NTkgMjguMTA5NCA4Ni45NDg0IDI4LjE1NDZDMTQzLjQ2IDI3Ljk5NDEgMTk5Ljk3MSAyNy45NjEgMjU2LjQ4MyAyOC4wNTU0Wk0yMTAuOTI2IDMzOS42NDNDMjEwLjkyNiAzNTQuNjcgMjEwLjkyNiAzNjkuNjk3IDIxMC45MjYgMzg0LjczOEMyMTAuNzczIDM4OC4yMDUgMjExLjM0MyAzOTEuNjY1IDIxMi41OTcgMzk0Ljg5OUMyMTMuODUyIDM5OC4xMzQgMjE1Ljc2NCA0MDEuMDcxIDIxOC4yMTMgNDAzLjUyNUMyMjAuNjYyIDQwNS45NzkgMjIzLjU5MyA0MDcuODk1IDIyNi44MjEgNDA5LjE1MkMyMzAuMDQ5IDQxMC40MDkgMjMzLjUwMyA0MTAuOTc5IDIzNi45NjIgNDEwLjgyNkMyNDkuMzg3IDQxMC44MjYgMjYxLjgxMiA0MTAuODI2IDI3NC4yMzYgNDEwLjgyNkMyNzcuOTIyIDQxMS4xODMgMjgxLjY0MiA0MTAuNzE3IDI4NS4xMjcgNDA5LjQ2MkMyODguNjEyIDQwOC4yMDggMjkxLjc3NyA0MDYuMTk2IDI5NC4zOTQgNDAzLjU3QzI5Ny4wMTIgNDAwLjk0NSAyOTkuMDE3IDM5Ny43NzIgMzAwLjI2NSAzOTQuMjc4QzMwMS41MTQgMzkwLjc4NSAzMDEuOTc1IDM4Ny4wNTggMzAxLjYxNSAzODMuMzY0QzMwMS42MTUgMzUzLjkxOSAzMDEuNjE2IDMyNC40NiAzMDEuNDc0IDI5NS4wMTVDMzAxLjMxMSAyOTMuMzMxIDMwMS42NyAyOTEuNjM3IDMwMi41MDIgMjkwLjE2NUMzMDMuMzM0IDI4OC42OTIgMzA0LjU5OSAyODcuNTEyIDMwNi4xMjUgMjg2Ljc4NkMzMjMuNzkgMjc2LjI5OCAzMzcuNTUxIDI2MC4zMTQgMzQ1LjMxMyAyNDEuMjY2QzM1My4wNzUgMjIyLjIxOSAzNTQuNDEzIDIwMS4xNTEgMzQ5LjEyMyAxODEuMjcyQzM0Mi4zNTYgMTU2Ljg1MyAzMjYuMjg2IDEzNi4wNzUgMzA0LjM3NiAxMjMuNDE0QzI4Mi40NjYgMTEwLjc1NCAyNTYuNDY5IDEwNy4yMjUgMjMxLjk4NyAxMTMuNTg2QzIxNy42NjkgMTE2LjU0NCAyMDQuMjg5IDEyMi45NTkgMTkzLjAwNyAxMzIuMjc0QzE4MS43MjYgMTQxLjU4OCAxNzIuODg0IDE1My41MjIgMTY3LjI0OSAxNjcuMDM4QzE1OS4wMjcgMTg4LjY4NiAxNTguNTQ4IDIxMi41MjEgMTY1Ljg5MyAyMzQuNDg0QzE3My4yMzggMjU2LjQ0NyAxODcuOTU0IDI3NS4xODEgMjA3LjUzMyAyODcuNDk1QzIwOC42NyAyODguMDM4IDIwOS42MTMgMjg4LjkxNyAyMTAuMjM3IDI5MC4wMTNDMjEwLjg2MSAyOTEuMTA5IDIxMS4xMzYgMjkyLjM3IDIxMS4wMjUgMjkzLjYyN0MyMTAuODQxIDMwOS4wMDggMjEwLjkyNiAzMjQuMzMzIDIxMC45MjYgMzM5LjY3MVYzMzkuNjQzWiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yNTYuNDgzIDI4LjA1NTRDMzEzLjg5OSAyOC4wNTU0IDM3MS4zMTUgMjcuODg1NiA0MjguNzQ1IDI4LjE0MDVDNDQwLjY4IDI3LjkwNzYgNDUyLjUyMSAzMC4yODg5IDQ2My40NDEgMzUuMTE3OUM0NzQuMzYyIDM5Ljk0NjkgNDg0LjA5OSA0Ny4xMDczIDQ5MS45NzEgNTYuMDk4NEM1MDQuMDYyIDY5LjY5NjIgNTExLjEzMiA4Ny4wMzg3IDUxMiAxMDUuMjNDNTEyLjAyOCAxMjEuOTMzIDUxMC4wMzUgMTM4LjU3OCA1MDYuMDYzIDE1NC44MDFDNDk4LjQ0NCAxOTguNzA2IDQ5MC41MTUgMjQyLjUyNyA0ODIuNzI2IDI4Ni4zNzZDNDc2LjAxMiAzMjQuMTM0IDQ2OS41ODEgMzYxLjk1IDQ2Mi41MTMgMzk5LjY4QzQ1Ny42NzIgNDIwLjk2OSA0NDYuNTQ1IDQ0MC4zMDMgNDMwLjU4IDQ1NS4xNjVDNDE0LjYxNiA0NzAuMDI3IDM5NC41NTUgNDc5LjcyNyAzNzMuMDExIDQ4My4wMDJDMzY3Ljc1MiA0ODMuNjI5IDM2Mi40NjIgNDgzLjk0NiAzNTcuMTY2IDQ4My45NUMyOTAuMDUzIDQ4NC4wMTcgMjIyLjk0IDQ4NC4wMTcgMTU1LjgyOCA0ODMuOTVDMTMwLjQ2NiA0ODMuOSAxMDUuOTMgNDc0LjkxNSA4Ni41MTMyIDQ1OC41NjZDNjcuMDk2NSA0NDIuMjE4IDU0LjAzNjIgNDE5LjU0OCA0OS42MTggMzk0LjUyNUMzNi4xODA0IDMxOS4xNzcgMjIuNjI5NyAyNDMuODUzIDguOTY1OTcgMTY4LjU1M0M2LjI4MDM0IDE1My42MzkgMy4zMTIgMTM4LjgxMSAxLjIwNTkgMTIzLjc4NEMtMi40NjEwNSAxMDIuNzI5IDIuMzEwOTMgODEuMDc0NCAxNC40ODUyIDYzLjUyNDRDMjYuNjU5NiA0NS45NzQ1IDQ1LjI1MjkgMzMuOTQ2MiA2Ni4yMjY2IDMwLjA1MjVDNzMuMDU1NyAyOC43NDUxIDc5Ljk5NTkgMjguMTA5NCA4Ni45NDg0IDI4LjE1NDZDMTQzLjQ2IDI3Ljk5NDEgMTk5Ljk3MSAyNy45NjEgMjU2LjQ4MyAyOC4wNTU0Wk0yMTAuOTI2IDMzOS42NDNDMjEwLjkyNiAzNTQuNjcgMjEwLjkyNiAzNjkuNjk3IDIxMC45MjYgMzg0LjczOEMyMTAuNzczIDM4OC4yMDUgMjExLjM0MyAzOTEuNjY1IDIxMi41OTcgMzk0Ljg5OUMyMTMuODUyIDM5OC4xMzQgMjE1Ljc2NCA0MDEuMDcxIDIxOC4yMTMgNDAzLjUyNUMyMjAuNjYyIDQwNS45NzkgMjIzLjU5MyA0MDcuODk1IDIyNi44MjEgNDA5LjE1MkMyMzAuMDQ5IDQxMC40MDkgMjMzLjUwMyA0MTAuOTc5IDIzNi45NjIgNDEwLjgyNkMyNDkuMzg3IDQxMC44MjYgMjYxLjgxMiA0MTAuODI2IDI3NC4yMzYgNDEwLjgyNkMyNzcuOTIyIDQxMS4xODMgMjgxLjY0MiA0MTAuNzE3IDI4NS4xMjcgNDA5LjQ2MkMyODguNjEyIDQwOC4yMDggMjkxLjc3NyA0MDYuMTk2IDI5NC4zOTQgNDAzLjU3QzI5Ny4wMTIgNDAwLjk0NSAyOTkuMDE3IDM5Ny43NzIgMzAwLjI2NSAzOTQuMjc4QzMwMS41MTQgMzkwLjc4NSAzMDEuOTc1IDM4Ny4wNTggMzAxLjYxNSAzODMuMzY0QzMwMS42MTUgMzUzLjkxOSAzMDEuNjE2IDMyNC40NiAzMDEuNDc0IDI5NS4wMTVDMzAxLjMxMSAyOTMuMzMxIDMwMS42NyAyOTEuNjM3IDMwMi41MDIgMjkwLjE2NUMzMDMuMzM0IDI4OC42OTIgMzA0LjU5OSAyODcuNTEyIDMwNi4xMjUgMjg2Ljc4NkMzMjMuNzkgMjc2LjI5OCAzMzcuNTUxIDI2MC4zMTQgMzQ1LjMxMyAyNDEuMjY2QzM1My4wNzUgMjIyLjIxOSAzNTQuNDEzIDIwMS4xNTEgMzQ5LjEyMyAxODEuMjcyQzM0Mi4zNTYgMTU2Ljg1MyAzMjYuMjg2IDEzNi4wNzUgMzA0LjM3NiAxMjMuNDE0QzI4Mi40NjYgMTEwLjc1NCAyNTYuNDY5IDEwNy4yMjUgMjMxLjk4NyAxMTMuNTg2QzIxNy42NjkgMTE2LjU0NCAyMDQuMjg5IDEyMi45NTkgMTkzLjAwNyAxMzIuMjc0QzE4MS43MjYgMTQxLjU4OCAxNzIuODg0IDE1My41MjIgMTY3LjI0OSAxNjcuMDM4QzE1OS4wMjcgMTg4LjY4NiAxNTguNTQ4IDIxMi41MjEgMTY1Ljg5MyAyMzQuNDg0QzE3My4yMzggMjU2LjQ0NyAxODcuOTU0IDI3NS4xODEgMjA3LjUzMyAyODcuNDk1QzIwOC42NyAyODguMDM4IDIwOS42MTMgMjg4LjkxNyAyMTAuMjM3IDI5MC4wMTNDMjEwLjg2MSAyOTEuMTA5IDIxMS4xMzYgMjkyLjM3IDIxMS4wMjUgMjkzLjYyN0MyMTAuODQxIDMwOS4wMDggMjEwLjkyNiAzMjQuMzMzIDIxMC45MjYgMzM5LjY3MVYzMzkuNjQzWiIgZmlsbD0iIzBEMzM4RiIvPgo8L3N2Zz4K" + }, + "b5397666-4885-aa6b-cebf-e52262a439a2": { + "name": "Chromium Browser" + }, + "771b48fd-d3d4-4f74-9232-fc157ab0507a": { + "name": "Edge on Mac", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOnVybCgjbGluZWFyLWdyYWRpZW50KTt9LmNscy0ye29wYWNpdHk6MC4zNTtmaWxsOnVybCgjcmFkaWFsLWdyYWRpZW50KTt9LmNscy0yLC5jbHMtNHtpc29sYXRpb246aXNvbGF0ZTt9LmNscy0ze2ZpbGw6dXJsKCNsaW5lYXItZ3JhZGllbnQtMik7fS5jbHMtNHtvcGFjaXR5OjAuNDE7ZmlsbDp1cmwoI3JhZGlhbC1ncmFkaWVudC0yKTt9LmNscy01e2ZpbGw6dXJsKCNyYWRpYWwtZ3JhZGllbnQtMyk7fS5jbHMtNntmaWxsOnVybCgjcmFkaWFsLWdyYWRpZW50LTQpO308L3N0eWxlPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50IiB4MT0iNjMuMzMiIHkxPSI4NC4wMyIgeDI9IjI0MS42NyIgeTI9Ijg0LjAzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIC0xLCAwLCAyNjYpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGM1OWE0Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTE0YThiIi8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudCIgY3g9IjE2MS44MyIgY3k9IjY4LjkxIiByPSI5NS4zOCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLCAwLCAwLCAtMC45NSwgMCwgMjQ4Ljg0KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMC43MiIgc3RvcC1vcGFjaXR5PSIwIi8+PHN0b3Agb2Zmc2V0PSIwLjk1IiBzdG9wLW9wYWNpdHk9IjAuNTMiLz48c3RvcCBvZmZzZXQ9IjEiLz48L3JhZGlhbEdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTIiIHgxPSIxNTcuMzUiIHkxPSIxNjEuMzkiIHgyPSI0NS45NiIgeTI9IjQwLjA2IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIC0xLCAwLCAyNjYpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMWI5ZGUyIi8+PHN0b3Agb2Zmc2V0PSIwLjE2IiBzdG9wLWNvbG9yPSIjMTU5NWRmIi8+PHN0b3Agb2Zmc2V0PSIwLjY3IiBzdG9wLWNvbG9yPSIjMDY4MGQ3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDA3OGQ0Ii8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudC0yIiBjeD0iLTM0MC4yOSIgY3k9IjYyLjk5IiByPSIxNDMuMjQiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMC4xNSwgLTAuOTksIC0wLjgsIC0wLjEyLCAxNzYuNjQsIC0xMjUuNCkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAuNzYiIHN0b3Atb3BhY2l0eT0iMCIvPjxzdG9wIG9mZnNldD0iMC45NSIgc3RvcC1vcGFjaXR5PSIwLjUiLz48c3RvcCBvZmZzZXQ9IjEiLz48L3JhZGlhbEdyYWRpZW50PjxyYWRpYWxHcmFkaWVudCBpZD0icmFkaWFsLWdyYWRpZW50LTMiIGN4PSIxMTMuMzciIGN5PSI1NzAuMjEiIHI9IjIwMi40MyIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgtMC4wNCwgMSwgMi4xMywgMC4wOCwgLTExNzkuNTQsIC0xMDYuNjkpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMzVjMWYxIi8+PHN0b3Agb2Zmc2V0PSIwLjExIiBzdG9wLWNvbG9yPSIjMzRjMWVkIi8+PHN0b3Agb2Zmc2V0PSIwLjIzIiBzdG9wLWNvbG9yPSIjMmZjMmRmIi8+PHN0b3Agb2Zmc2V0PSIwLjMxIiBzdG9wLWNvbG9yPSIjMmJjM2QyIi8+PHN0b3Agb2Zmc2V0PSIwLjY3IiBzdG9wLWNvbG9yPSIjMzZjNzUyIi8+PC9yYWRpYWxHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudC00IiBjeD0iMzc2LjUyIiBjeT0iNTY3Ljk3IiByPSI5Ny4zNCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgwLjI4LCAwLjk2LCAwLjc4LCAtMC4yMywgLTMwMy43NiwgLTE0OC41KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzY2ZWI2ZSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzY2ZWI2ZSIgc3RvcC1vcGFjaXR5PSIwIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHRpdGxlPkVkZ2VfTG9nb18yNjV4MjY1PC90aXRsZT48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0yMzUuNjgsMTk1LjQ2YTkzLjczLDkzLjczLDAsMCwxLTEwLjU0LDQuNzEsMTAxLjg3LDEwMS44NywwLDAsMS0zNS45LDYuNDZjLTQ3LjMyLDAtODguNTQtMzIuNTUtODguNTQtNzQuMzJBMzEuNDgsMzEuNDgsMCwwLDEsMTE3LjEzLDEwNWMtNDIuOCwxLjgtNTMuOCw0Ni40LTUzLjgsNzIuNTMsMCw3My44OCw2OC4wOSw4MS4zNyw4Mi43Niw4MS4zNyw3LjkxLDAsMTkuODQtMi4zLDI3LTQuNTZsMS4zMS0uNDRBMTI4LjM0LDEyOC4zNCwwLDAsMCwyNDEsMjAxLjEsNCw0LDAsMCwwLDIzNS42OCwxOTUuNDZaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMzUuNjgsMTk1LjQ2YTkzLjczLDkzLjczLDAsMCwxLTEwLjU0LDQuNzEsMTAxLjg3LDEwMS44NywwLDAsMS0zNS45LDYuNDZjLTQ3LjMyLDAtODguNTQtMzIuNTUtODguNTQtNzQuMzJBMzEuNDgsMzEuNDgsMCwwLDEsMTE3LjEzLDEwNWMtNDIuOCwxLjgtNTMuOCw0Ni40LTUzLjgsNzIuNTMsMCw3My44OCw2OC4wOSw4MS4zNyw4Mi43Niw4MS4zNyw3LjkxLDAsMTkuODQtMi4zLDI3LTQuNTZsMS4zMS0uNDRBMTI4LjM0LDEyOC4zNCwwLDAsMCwyNDEsMjAxLjEsNCw0LDAsMCwwLDIzNS42OCwxOTUuNDZaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik0xMTAuMzQsMjQ2LjM0QTc5LjIsNzkuMiwwLDAsMSw4Ny42LDIyNSw4MC43Miw4MC43MiwwLDAsMSwxMTcuMTMsMTA1YzMuMTItMS40Nyw4LjQ1LTQuMTMsMTUuNTQtNGEzMi4zNSwzMi4zNSwwLDAsMSwyNS42OSwxMywzMS44OCwzMS44OCwwLDAsMSw2LjM2LDE4LjY2YzAtLjIxLDI0LjQ2LTc5LjYtODAtNzkuNi00My45LDAtODAsNDEuNjYtODAsNzguMjFhMTMwLjE1LDEzMC4xNSwwLDAsMCwxMi4xMSw1NiwxMjgsMTI4LDAsMCwwLDE1Ni4zOCw2Ny4xMSw3NS41NSw3NS41NSwwLDAsMS02Mi43OC04WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQuNjMgLTQuOTIpIi8+PHBhdGggY2xhc3M9ImNscy00IiBkPSJNMTEwLjM0LDI0Ni4zNEE3OS4yLDc5LjIsMCwwLDEsODcuNiwyMjUsODAuNzIsODAuNzIsMCwwLDEsMTE3LjEzLDEwNWMzLjEyLTEuNDcsOC40NS00LjEzLDE1LjU0LTRhMzIuMzUsMzIuMzUsMCwwLDEsMjUuNjksMTMsMzEuODgsMzEuODgsMCwwLDEsNi4zNiwxOC42NmMwLS4yMSwyNC40Ni03OS42LTgwLTc5LjYtNDMuOSwwLTgwLDQxLjY2LTgwLDc4LjIxYTEzMC4xNSwxMzAuMTUsMCwwLDAsMTIuMTEsNTYsMTI4LDEyOCwwLDAsMCwxNTYuMzgsNjcuMTEsNzUuNTUsNzUuNTUsMCwwLDEtNjIuNzgtOFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC00LjYzIC00LjkyKSIvPjxwYXRoIGNsYXNzPSJjbHMtNSIgZD0iTTE1Ni45NCwxNTMuNzhjLS44MSwxLjA1LTMuMywyLjUtMy4zLDUuNjYsMCwyLjYxLDEuNyw1LjEyLDQuNzIsNy4yMywxNC4zOCwxMCw0MS40OSw4LjY4LDQxLjU2LDguNjhBNTkuNTYsNTkuNTYsMCwwLDAsMjMwLjE5LDE2N2E2MS4zOCw2MS4zOCwwLDAsMCwzMC40My01Mi44OGMuMjYtMjIuNDEtOC0zNy4zMS0xMS4zNC00My45MUMyMjguMDksMjguNzYsMTgyLjM1LDQuOTIsMTMyLjYxLDQuOTJhMTI4LDEyOCwwLDAsMC0xMjgsMTI2LjJjLjQ4LTM2LjU0LDM2LjgtNjYuMDUsODAtNjYuMDUsMy41LDAsMjMuNDYuMzQsNDIsMTAuMDcsMTYuMzQsOC41OCwyNC45LDE4Ljk0LDMwLjg1LDI5LjIxLDYuMTgsMTAuNjcsNy4yOCwyNC4xNSw3LjI4LDI5LjUyUzE2MiwxNDcuMiwxNTYuOTQsMTUzLjc4WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQuNjMgLTQuOTIpIi8+PHBhdGggY2xhc3M9ImNscy02IiBkPSJNMTU2Ljk0LDE1My43OGMtLjgxLDEuMDUtMy4zLDIuNS0zLjMsNS42NiwwLDIuNjEsMS43LDUuMTIsNC43Miw3LjIzLDE0LjM4LDEwLDQxLjQ5LDguNjgsNDEuNTYsOC42OEE1OS41Niw1OS41NiwwLDAsMCwyMzAuMTksMTY3YTYxLjM4LDYxLjM4LDAsMCwwLDMwLjQzLTUyLjg4Yy4yNi0yMi40MS04LTM3LjMxLTExLjM0LTQzLjkxQzIyOC4wOSwyOC43NiwxODIuMzUsNC45MiwxMzIuNjEsNC45MmExMjgsMTI4LDAsMCwwLTEyOCwxMjYuMmMuNDgtMzYuNTQsMzYuOC02Ni4wNSw4MC02Ni4wNSwzLjUsMCwyMy40Ni4zNCw0MiwxMC4wNywxNi4zNCw4LjU4LDI0LjksMTguOTQsMzAuODUsMjkuMjEsNi4xOCwxMC42Nyw3LjI4LDI0LjE1LDcuMjgsMjkuNTJTMTYyLDE0Ny4yLDE1Ni45NCwxNTMuNzhaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48L3N2Zz4=", + "icon_light": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOnVybCgjbGluZWFyLWdyYWRpZW50KTt9LmNscy0ye29wYWNpdHk6MC4zNTtmaWxsOnVybCgjcmFkaWFsLWdyYWRpZW50KTt9LmNscy0yLC5jbHMtNHtpc29sYXRpb246aXNvbGF0ZTt9LmNscy0ze2ZpbGw6dXJsKCNsaW5lYXItZ3JhZGllbnQtMik7fS5jbHMtNHtvcGFjaXR5OjAuNDE7ZmlsbDp1cmwoI3JhZGlhbC1ncmFkaWVudC0yKTt9LmNscy01e2ZpbGw6dXJsKCNyYWRpYWwtZ3JhZGllbnQtMyk7fS5jbHMtNntmaWxsOnVybCgjcmFkaWFsLWdyYWRpZW50LTQpO308L3N0eWxlPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50IiB4MT0iNjMuMzMiIHkxPSI4NC4wMyIgeDI9IjI0MS42NyIgeTI9Ijg0LjAzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIC0xLCAwLCAyNjYpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGM1OWE0Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTE0YThiIi8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudCIgY3g9IjE2MS44MyIgY3k9IjY4LjkxIiByPSI5NS4zOCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLCAwLCAwLCAtMC45NSwgMCwgMjQ4Ljg0KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMC43MiIgc3RvcC1vcGFjaXR5PSIwIi8+PHN0b3Agb2Zmc2V0PSIwLjk1IiBzdG9wLW9wYWNpdHk9IjAuNTMiLz48c3RvcCBvZmZzZXQ9IjEiLz48L3JhZGlhbEdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTIiIHgxPSIxNTcuMzUiIHkxPSIxNjEuMzkiIHgyPSI0NS45NiIgeTI9IjQwLjA2IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIC0xLCAwLCAyNjYpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMWI5ZGUyIi8+PHN0b3Agb2Zmc2V0PSIwLjE2IiBzdG9wLWNvbG9yPSIjMTU5NWRmIi8+PHN0b3Agb2Zmc2V0PSIwLjY3IiBzdG9wLWNvbG9yPSIjMDY4MGQ3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDA3OGQ0Ii8+PC9saW5lYXJHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudC0yIiBjeD0iLTM0MC4yOSIgY3k9IjYyLjk5IiByPSIxNDMuMjQiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMC4xNSwgLTAuOTksIC0wLjgsIC0wLjEyLCAxNzYuNjQsIC0xMjUuNCkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAuNzYiIHN0b3Atb3BhY2l0eT0iMCIvPjxzdG9wIG9mZnNldD0iMC45NSIgc3RvcC1vcGFjaXR5PSIwLjUiLz48c3RvcCBvZmZzZXQ9IjEiLz48L3JhZGlhbEdyYWRpZW50PjxyYWRpYWxHcmFkaWVudCBpZD0icmFkaWFsLWdyYWRpZW50LTMiIGN4PSIxMTMuMzciIGN5PSI1NzAuMjEiIHI9IjIwMi40MyIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgtMC4wNCwgMSwgMi4xMywgMC4wOCwgLTExNzkuNTQsIC0xMDYuNjkpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMzVjMWYxIi8+PHN0b3Agb2Zmc2V0PSIwLjExIiBzdG9wLWNvbG9yPSIjMzRjMWVkIi8+PHN0b3Agb2Zmc2V0PSIwLjIzIiBzdG9wLWNvbG9yPSIjMmZjMmRmIi8+PHN0b3Agb2Zmc2V0PSIwLjMxIiBzdG9wLWNvbG9yPSIjMmJjM2QyIi8+PHN0b3Agb2Zmc2V0PSIwLjY3IiBzdG9wLWNvbG9yPSIjMzZjNzUyIi8+PC9yYWRpYWxHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9InJhZGlhbC1ncmFkaWVudC00IiBjeD0iMzc2LjUyIiBjeT0iNTY3Ljk3IiByPSI5Ny4zNCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgwLjI4LCAwLjk2LCAwLjc4LCAtMC4yMywgLTMwMy43NiwgLTE0OC41KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzY2ZWI2ZSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzY2ZWI2ZSIgc3RvcC1vcGFjaXR5PSIwIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHRpdGxlPkVkZ2VfTG9nb18yNjV4MjY1PC90aXRsZT48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0yMzUuNjgsMTk1LjQ2YTkzLjczLDkzLjczLDAsMCwxLTEwLjU0LDQuNzEsMTAxLjg3LDEwMS44NywwLDAsMS0zNS45LDYuNDZjLTQ3LjMyLDAtODguNTQtMzIuNTUtODguNTQtNzQuMzJBMzEuNDgsMzEuNDgsMCwwLDEsMTE3LjEzLDEwNWMtNDIuOCwxLjgtNTMuOCw0Ni40LTUzLjgsNzIuNTMsMCw3My44OCw2OC4wOSw4MS4zNyw4Mi43Niw4MS4zNyw3LjkxLDAsMTkuODQtMi4zLDI3LTQuNTZsMS4zMS0uNDRBMTI4LjM0LDEyOC4zNCwwLDAsMCwyNDEsMjAxLjEsNCw0LDAsMCwwLDIzNS42OCwxOTUuNDZaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMzUuNjgsMTk1LjQ2YTkzLjczLDkzLjczLDAsMCwxLTEwLjU0LDQuNzEsMTAxLjg3LDEwMS44NywwLDAsMS0zNS45LDYuNDZjLTQ3LjMyLDAtODguNTQtMzIuNTUtODguNTQtNzQuMzJBMzEuNDgsMzEuNDgsMCwwLDEsMTE3LjEzLDEwNWMtNDIuOCwxLjgtNTMuOCw0Ni40LTUzLjgsNzIuNTMsMCw3My44OCw2OC4wOSw4MS4zNyw4Mi43Niw4MS4zNyw3LjkxLDAsMTkuODQtMi4zLDI3LTQuNTZsMS4zMS0uNDRBMTI4LjM0LDEyOC4zNCwwLDAsMCwyNDEsMjAxLjEsNCw0LDAsMCwwLDIzNS42OCwxOTUuNDZaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik0xMTAuMzQsMjQ2LjM0QTc5LjIsNzkuMiwwLDAsMSw4Ny42LDIyNSw4MC43Miw4MC43MiwwLDAsMSwxMTcuMTMsMTA1YzMuMTItMS40Nyw4LjQ1LTQuMTMsMTUuNTQtNGEzMi4zNSwzMi4zNSwwLDAsMSwyNS42OSwxMywzMS44OCwzMS44OCwwLDAsMSw2LjM2LDE4LjY2YzAtLjIxLDI0LjQ2LTc5LjYtODAtNzkuNi00My45LDAtODAsNDEuNjYtODAsNzguMjFhMTMwLjE1LDEzMC4xNSwwLDAsMCwxMi4xMSw1NiwxMjgsMTI4LDAsMCwwLDE1Ni4zOCw2Ny4xMSw3NS41NSw3NS41NSwwLDAsMS02Mi43OC04WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQuNjMgLTQuOTIpIi8+PHBhdGggY2xhc3M9ImNscy00IiBkPSJNMTEwLjM0LDI0Ni4zNEE3OS4yLDc5LjIsMCwwLDEsODcuNiwyMjUsODAuNzIsODAuNzIsMCwwLDEsMTE3LjEzLDEwNWMzLjEyLTEuNDcsOC40NS00LjEzLDE1LjU0LTRhMzIuMzUsMzIuMzUsMCwwLDEsMjUuNjksMTMsMzEuODgsMzEuODgsMCwwLDEsNi4zNiwxOC42NmMwLS4yMSwyNC40Ni03OS42LTgwLTc5LjYtNDMuOSwwLTgwLDQxLjY2LTgwLDc4LjIxYTEzMC4xNSwxMzAuMTUsMCwwLDAsMTIuMTEsNTYsMTI4LDEyOCwwLDAsMCwxNTYuMzgsNjcuMTEsNzUuNTUsNzUuNTUsMCwwLDEtNjIuNzgtOFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC00LjYzIC00LjkyKSIvPjxwYXRoIGNsYXNzPSJjbHMtNSIgZD0iTTE1Ni45NCwxNTMuNzhjLS44MSwxLjA1LTMuMywyLjUtMy4zLDUuNjYsMCwyLjYxLDEuNyw1LjEyLDQuNzIsNy4yMywxNC4zOCwxMCw0MS40OSw4LjY4LDQxLjU2LDguNjhBNTkuNTYsNTkuNTYsMCwwLDAsMjMwLjE5LDE2N2E2MS4zOCw2MS4zOCwwLDAsMCwzMC40My01Mi44OGMuMjYtMjIuNDEtOC0zNy4zMS0xMS4zNC00My45MUMyMjguMDksMjguNzYsMTgyLjM1LDQuOTIsMTMyLjYxLDQuOTJhMTI4LDEyOCwwLDAsMC0xMjgsMTI2LjJjLjQ4LTM2LjU0LDM2LjgtNjYuMDUsODAtNjYuMDUsMy41LDAsMjMuNDYuMzQsNDIsMTAuMDcsMTYuMzQsOC41OCwyNC45LDE4Ljk0LDMwLjg1LDI5LjIxLDYuMTgsMTAuNjcsNy4yOCwyNC4xNSw3LjI4LDI5LjUyUzE2MiwxNDcuMiwxNTYuOTQsMTUzLjc4WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQuNjMgLTQuOTIpIi8+PHBhdGggY2xhc3M9ImNscy02IiBkPSJNMTU2Ljk0LDE1My43OGMtLjgxLDEuMDUtMy4zLDIuNS0zLjMsNS42NiwwLDIuNjEsMS43LDUuMTIsNC43Miw3LjIzLDE0LjM4LDEwLDQxLjQ5LDguNjgsNDEuNTYsOC42OEE1OS41Niw1OS41NiwwLDAsMCwyMzAuMTksMTY3YTYxLjM4LDYxLjM4LDAsMCwwLDMwLjQzLTUyLjg4Yy4yNi0yMi40MS04LTM3LjMxLTExLjM0LTQzLjkxQzIyOC4wOSwyOC43NiwxODIuMzUsNC45MiwxMzIuNjEsNC45MmExMjgsMTI4LDAsMCwwLTEyOCwxMjYuMmMuNDgtMzYuNTQsMzYuOC02Ni4wNSw4MC02Ni4wNSwzLjUsMCwyMy40Ni4zNCw0MiwxMC4wNywxNi4zNCw4LjU4LDI0LjksMTguOTQsMzAuODUsMjkuMjEsNi4xOCwxMC42Nyw3LjI4LDI0LjE1LDcuMjgsMjkuNTJTMTYyLDE0Ny4yLDE1Ni45NCwxNTMuNzhaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNC42MyAtNC45MikiLz48L3N2Zz4=" + }, + "39a5647e-1853-446c-a1f6-a79bae9f5bc7": { + "name": "IDmelon", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2YxNWI1Yzt9LmNscy0ye2ZpbGw6IzkyMWIxZDt9LmNscy0ze2ZpbGw6I2VlMzAyNTt9LmNscy00e2ZpbGw6I2JiMjAyNjt9PC9zdHlsZT48L2RlZnM+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMzQxLjg3LDEwMC4ybC00LjI5LTEuNjRjLTMyLjMxLTExLjgxLTY1LjM2LTEzLjI3LTc2LjkyLTEzLjRsLS44OSwwSDEyMC4xMkEyMy40MywyMy40MywwLDAsMCwxMTEuNiw4N2MtLjQxLjIxLS44MS40Mi0xLjE3LjY0bC0xLjg1LDEuNzYsMTMzLjM1LDY1LjgsMTAzLjM4LTUyLjg5WiIvPjxsaW5lIGNsYXNzPSJjbHMtMiIgeDE9IjI5NC41OCIgeTE9IjEzNy4wNyIgeDI9IjI5Ni45OSIgeTI9IjEzOC4yNyIvPjxsaW5lIGNsYXNzPSJjbHMtMiIgeDE9IjIzOS41MyIgeTE9IjE1Mi4xMSIgeDI9IjI0MS45MyIgeTI9IjE1My4zMSIvPjxwYXRoIGNsYXNzPSJjbHMtMyIgZD0iTTEwNi43NCw5MXEtMi42Miw0LjItMi4zNSwxMS4yNnQuMjYsMTMuMzdWNDIzLjIxcTAsNS43Ni0uMjYsMTQuNDF0MS4zMSwxMi44NGExNC41NSwxNC41NSwwLDAsMCwxLjE0LDIuMTlsMTM2LTI5OS41NkwxMTAuNDMsODcuNjVBMTEuMjQsMTEuMjQsMCwwLDAsMTA2Ljc0LDkxWiIvPjxwYXRoIGNsYXNzPSJjbHMtNCIgZD0iTTM2MS44NiwxMTEuNTNjLTIuMzItMS41NS00LjctMy4xLTcuMTMtNC42OGE5My45Miw5My45MiwwLDAsMC0xMi02LjMyYy0uMjctLjExLS41NS0uMjMtLjgzLS4zM2wtOTksNTIuODlMMzg3LjYzLDQwMi4zMUExNjQuMDcsMTY0LjA3LDAsMCwwLDM5NywzODguMTJxMjkuODItNTEuMjEsMjkuODItMTI1YTI4NC44MywyODQuODMsMCwwLDAtNy4wOC02MS4yNSwxNjQuMTYsMTY0LjE2LDAsMCwwLTI2LjUzLTU5Ljc1LDEzNC45LDEzNC45LDAsMCwwLTkuMDUtMTEuMzhBMTUzLjIsMTUzLjIsMCwwLDAsMzYxLjg2LDExMS41M1oiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xMDYuNjUsNDUyLjM0YTEwLjA3LDEwLjA3LDAsMCwwLDcuNjksNS4xOWwxLjc0LjJoMTU2YzUwLjI3LDAsODguNjQtMTguNjksMTE1LjUyLTU1LjQyTDI0Mi44OSwxNTMuMDlaIi8+PC9zdmc+", + "icon_light": "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2YxNWI1Yzt9LmNscy0ye2ZpbGw6IzkyMWIxZDt9LmNscy0ze2ZpbGw6I2VlMzAyNTt9LmNscy00e2ZpbGw6I2JiMjAyNjt9PC9zdHlsZT48L2RlZnM+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMzQxLjg3LDEwMC4ybC00LjI5LTEuNjRjLTMyLjMxLTExLjgxLTY1LjM2LTEzLjI3LTc2LjkyLTEzLjRsLS44OSwwSDEyMC4xMkEyMy40MywyMy40MywwLDAsMCwxMTEuNiw4N2MtLjQxLjIxLS44MS40Mi0xLjE3LjY0bC0xLjg1LDEuNzYsMTMzLjM1LDY1LjgsMTAzLjM4LTUyLjg5WiIvPjxsaW5lIGNsYXNzPSJjbHMtMiIgeDE9IjI5NC41OCIgeTE9IjEzNy4wNyIgeDI9IjI5Ni45OSIgeTI9IjEzOC4yNyIvPjxsaW5lIGNsYXNzPSJjbHMtMiIgeDE9IjIzOS41MyIgeTE9IjE1Mi4xMSIgeDI9IjI0MS45MyIgeTI9IjE1My4zMSIvPjxwYXRoIGNsYXNzPSJjbHMtMyIgZD0iTTEwNi43NCw5MXEtMi42Miw0LjItMi4zNSwxMS4yNnQuMjYsMTMuMzdWNDIzLjIxcTAsNS43Ni0uMjYsMTQuNDF0MS4zMSwxMi44NGExNC41NSwxNC41NSwwLDAsMCwxLjE0LDIuMTlsMTM2LTI5OS41NkwxMTAuNDMsODcuNjVBMTEuMjQsMTEuMjQsMCwwLDAsMTA2Ljc0LDkxWiIvPjxwYXRoIGNsYXNzPSJjbHMtNCIgZD0iTTM2MS44NiwxMTEuNTNjLTIuMzItMS41NS00LjctMy4xLTcuMTMtNC42OGE5My45Miw5My45MiwwLDAsMC0xMi02LjMyYy0uMjctLjExLS41NS0uMjMtLjgzLS4zM2wtOTksNTIuODlMMzg3LjYzLDQwMi4zMUExNjQuMDcsMTY0LjA3LDAsMCwwLDM5NywzODguMTJxMjkuODItNTEuMjEsMjkuODItMTI1YTI4NC44MywyODQuODMsMCwwLDAtNy4wOC02MS4yNSwxNjQuMTYsMTY0LjE2LDAsMCwwLTI2LjUzLTU5Ljc1LDEzNC45LDEzNC45LDAsMCwwLTkuMDUtMTEuMzhBMTUzLjIsMTUzLjIsMCwwLDAsMzYxLjg2LDExMS41M1oiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xMDYuNjUsNDUyLjM0YTEwLjA3LDEwLjA3LDAsMCwwLDcuNjksNS4xOWwxLjc0LjJoMTU2YzUwLjI3LDAsODguNjQtMTguNjksMTE1LjUyLTU1LjQyTDI0Mi44OSwxNTMuMDlaIi8+PC9zdmc+" + }, + "d548826e-79b4-db40-a3d8-11116f7e8349": { + "name": "Bitwarden", + "icon_dark": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjAuMywgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9Ikljb24iIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDEwMjQgMTAyNDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiMxNzVEREM7fQoJLnN0MXtmaWxsOiNGRkZGRkY7fQo8L3N0eWxlPgo8cmVjdCBpZD0iQmFja2dyb3VuZCIgY2xhc3M9InN0MCIgd2lkdGg9IjEwMjQiIGhlaWdodD0iMTAyNCIvPgo8cGF0aCBpZD0iSWRlbnRpdHkiIGNsYXNzPSJzdDEiIGQ9Ik04MjkuOCwxMjguNmMtNi41LTYuNS0xNC4yLTkuNy0yMy05LjdIMjE3LjJjLTguOSwwLTE2LjUsMy4yLTIzLDkuN3MtOS43LDE0LjItOS43LDIzdjM5My4xCgljMCwyOS4zLDUuNyw1OC40LDE3LjEsODcuM2MxMS40LDI4LjgsMjUuNiw1NC40LDQyLjUsNzYuOGMxNi45LDIyLjMsMzcsNDQuMSw2MC40LDY1LjNzNDUsMzguNyw2NC43LDUyLjcKCWMxOS44LDE0LDQwLjQsMjcuMiw2MS45LDM5LjdzMzYuOCwyMC45LDQ1LjgsMjUuM2M5LDQuNCwxNi4zLDcuOSwyMS43LDEwLjJjNC4xLDIsOC41LDMuMSwxMy4zLDMuMWM0LjgsMCw5LjItMSwxMy4zLTMuMQoJYzUuNS0yLjQsMTIuNy01LjgsMjEuOC0xMC4yYzktNC40LDI0LjMtMTIuOSw0NS44LTI1LjNjMjEuNS0xMi41LDQyLjEtMjUuNyw2MS45LTM5LjdjMTkuOC0xNCw0MS40LTMxLjYsNjQuOC01Mi43CgljMjMuNC0yMS4yLDQzLjUtNDIuOSw2MC40LTY1LjNjMTYuOS0yMi40LDMxLTQ3LjksNDIuNS03Ni44YzExLjQtMjguOCwxNy4xLTU3LjksMTcuMS04Ny4zdi0zOTMKCUM4MzkuNiwxNDIuOCw4MzYuMywxMzUuMSw4MjkuOCwxMjguNnogTTc1My44LDU0OC40YzAsMTQyLjMtMjQxLjgsMjY0LjktMjQxLjgsMjY0LjlWMjAzaDI0MS44Qzc1My44LDIwMyw3NTMuOCw0MDYuMSw3NTMuOCw1NDguNHoKCSIvPgo8L3N2Zz4K", + "icon_light": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjAuMywgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9Ikljb24iIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDEwMjQgMTAyNDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiMxNzVEREM7fQoJLnN0MXtmaWxsOiNGRkZGRkY7fQo8L3N0eWxlPgo8cmVjdCBpZD0iQmFja2dyb3VuZCIgY2xhc3M9InN0MCIgd2lkdGg9IjEwMjQiIGhlaWdodD0iMTAyNCIvPgo8cGF0aCBpZD0iSWRlbnRpdHkiIGNsYXNzPSJzdDEiIGQ9Ik04MjkuOCwxMjguNmMtNi41LTYuNS0xNC4yLTkuNy0yMy05LjdIMjE3LjJjLTguOSwwLTE2LjUsMy4yLTIzLDkuN3MtOS43LDE0LjItOS43LDIzdjM5My4xCgljMCwyOS4zLDUuNyw1OC40LDE3LjEsODcuM2MxMS40LDI4LjgsMjUuNiw1NC40LDQyLjUsNzYuOGMxNi45LDIyLjMsMzcsNDQuMSw2MC40LDY1LjNzNDUsMzguNyw2NC43LDUyLjcKCWMxOS44LDE0LDQwLjQsMjcuMiw2MS45LDM5LjdzMzYuOCwyMC45LDQ1LjgsMjUuM2M5LDQuNCwxNi4zLDcuOSwyMS43LDEwLjJjNC4xLDIsOC41LDMuMSwxMy4zLDMuMWM0LjgsMCw5LjItMSwxMy4zLTMuMQoJYzUuNS0yLjQsMTIuNy01LjgsMjEuOC0xMC4yYzktNC40LDI0LjMtMTIuOSw0NS44LTI1LjNjMjEuNS0xMi41LDQyLjEtMjUuNyw2MS45LTM5LjdjMTkuOC0xNCw0MS40LTMxLjYsNjQuOC01Mi43CgljMjMuNC0yMS4yLDQzLjUtNDIuOSw2MC40LTY1LjNjMTYuOS0yMi40LDMxLTQ3LjksNDIuNS03Ni44YzExLjQtMjguOCwxNy4xLTU3LjksMTcuMS04Ny4zdi0zOTMKCUM4MzkuNiwxNDIuOCw4MzYuMywxMzUuMSw4MjkuOCwxMjguNnogTTc1My44LDU0OC40YzAsMTQyLjMtMjQxLjgsMjY0LjktMjQxLjgsMjY0LjlWMjAzaDI0MS44Qzc1My44LDIwMyw3NTMuOCw0MDYuMSw3NTMuOCw1NDguNHoKCSIvPgo8L3N2Zz4K" + }, + "fbfc3007-154e-4ecc-8c0b-6e020557d7bd": { + "name": "iCloud Keychain", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJub25lIj48cGF0aCBkPSJtMjE3LjM2LDkwLjY5Yy0xNS41OCw5LjU0LTI1LjE3LDI2LjQxLTI1LjM4LDQ0LjY4LjA2LDIwLjY3LDEyLjQzLDM5LjMyLDMxLjQ2LDQ3LjQxLTMuNjcsMTEuODQtOS4xLDIzLjA2LTE2LjExLDMzLjI4LTEwLjAzLDE0LjQ0LTIwLjUyLDI4Ljg3LTM2LjQ3LDI4Ljg3cy0yMC4wNi05LjI3LTM4LjQ1LTkuMjctMjQuMzIsOS41Ny0zOC45LDkuNTctMjQuNzctMTMuMzctMzYuNDctMjkuNzljLTE1LjQ2LTIyLjk5LTIzLjk1LTQ5Ljk2LTI0LjQ3LTc3LjY2LDAtNDUuNTksMjkuNjMtNjkuNzUsNTguODEtNjkuNzUsMTUuNSwwLDI4LjQyLDEwLjE4LDM4LjE1LDEwLjE4czIzLjcxLTEwLjc5LDQxLjM0LTEwLjc5YzE4LjQxLS40NywzNS44NCw4LjI0LDQ2LjUsMjMuMjVabS01NC44Ni00Mi41NWM3Ljc3LTkuMTQsMTIuMTctMjAuNjcsMTIuNDYtMzIuNjcuMDEtMS41OC0uMTQtMy4xNi0uNDYtNC43MS0xMy4zNSwxLjMtMjUuNjksNy42Ny0zNC41LDE3Ljc4LTcuODUsOC43OC0xMi40MSwyMC0xMi45MiwzMS43NiwwLDEuNDMuMTYsMi44Ni40Niw0LjI2LDEuMDUuMiwyLjEyLjMsMy4xOS4zLDEyLjQzLS45OSwyMy45MS03LjA0LDMxLjc2LTE2LjczWiIgZmlsbD0iI0ZGRiIvPjwvc3ZnPg==", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJub25lIj48cGF0aCBkPSJtMjE3LjM2LDkwLjY5Yy0xNS41OCw5LjU0LTI1LjE3LDI2LjQxLTI1LjM4LDQ0LjY4LjA2LDIwLjY3LDEyLjQzLDM5LjMyLDMxLjQ2LDQ3LjQxLTMuNjcsMTEuODQtOS4xLDIzLjA2LTE2LjExLDMzLjI4LTEwLjAzLDE0LjQ0LTIwLjUyLDI4Ljg3LTM2LjQ3LDI4Ljg3cy0yMC4wNi05LjI3LTM4LjQ1LTkuMjctMjQuMzIsOS41Ny0zOC45LDkuNTctMjQuNzctMTMuMzctMzYuNDctMjkuNzljLTE1LjQ2LTIyLjk5LTIzLjk1LTQ5Ljk2LTI0LjQ3LTc3LjY2LDAtNDUuNTksMjkuNjMtNjkuNzUsNTguODEtNjkuNzUsMTUuNSwwLDI4LjQyLDEwLjE4LDM4LjE1LDEwLjE4czIzLjcxLTEwLjc5LDQxLjM0LTEwLjc5YzE4LjQxLS40NywzNS44NCw4LjI0LDQ2LjUsMjMuMjVabS01NC44Ni00Mi41NWM3Ljc3LTkuMTQsMTIuMTctMjAuNjcsMTIuNDYtMzIuNjcuMDEtMS41OC0uMTQtMy4xNi0uNDYtNC43MS0xMy4zNSwxLjMtMjUuNjksNy42Ny0zNC41LDE3Ljc4LTcuODUsOC43OC0xMi40MSwyMC0xMi45MiwzMS43NiwwLDEuNDMuMTYsMi44Ni40Niw0LjI2LDEuMDUuMiwyLjEyLjMsMy4xOS4zLDEyLjQzLS45OSwyMy45MS03LjA0LDMxLjc2LTE2LjczWiIgZmlsbD0iIzAwMCIvPjwvc3ZnPg==" + }, + "53414d53-554e-4700-0000-000000000000": { + "name": "Samsung Pass", + "icon_dark": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgd2lkdGg9IjUycHgiIGhlaWdodD0iNTJweCIgdmlld0JveD0iMCAwIDUyLjAgNTIuMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PGNsaXBQYXRoIGlkPSJpMCI+PHBhdGggZD0iTTM2MCwwIEwzNjAsODAwIEwwLDgwMCBMMCwwIEwzNjAsMCBaIj48L3BhdGg+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImkxIj48cGF0aCBkPSJNMjYsMCBDMzMuOTkxMDI3OCwwIDQxLjEzOTU4MzMsMC45NzUgNDUuOTA4Nzc3OCw1Ljc3Nzc3Nzc4IEM0OS43MTAxOTQ0LDkuNjA1OTE2NjcgNTIsMTUuODY1MDU1NiA1MiwyNiBDNTIsMzYuMTM0OTQ0NCA0OS43MDk4MzMzLDQyLjM5NDQ0NDQgNDUuOTA4MDU1Niw0Ni4yMjI1ODMzIEM0MS4xMzg4NjExLDUxLjAyNDYzODkgMzMuOTkwMzA1Niw1MiAyNiw1MiBDMTguMDA4OTcyMiw1MiAxMC44NjA3Nzc4LDUxLjAyNDYzODkgNi4wOTE1ODMzMyw0Ni4yMjI1ODMzIEMyLjI5MDE2NjY3LDQyLjM5NDQ0NDQgMCwzNi4xMzQ5NDQ0IDAsMjYgQzAsMTUuODY1MDU1NiAyLjI4OTgwNTU2LDkuNjA1NTU1NTYgNi4wOTA4NjExMSw1Ljc3Nzc3Nzc4IEMxMC44NjAwNTU2LDAuOTc1IDE4LjAwODYxMTEsMCAyNiwwIFoiPjwvcGF0aD48L2NsaXBQYXRoPjxsaW5lYXJHcmFkaWVudCBpZD0iaTIiIHgxPSIyNnB4IiB5MT0iNTJweCIgeDI9IjI2cHgiIHkyPSIwLjE5NTkyMTE0OHB4IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agc3RvcC1jb2xvcj0iIzI5MjlCMiIgb2Zmc2V0PSIwJSI+PC9zdG9wPjxzdG9wIHN0b3AtY29sb3I9IiMxQTQwQ0MiIG9mZnNldD0iMTAwJSI+PC9zdG9wPjwvbGluZWFyR3JhZGllbnQ+PGNsaXBQYXRoIGlkPSJpMyI+PHBhdGggZD0iTTM3LjE5NDQ0NDQsMCBMMzcuMTk0NDQ0NCw1LjcyMjIyMjIyIEwwLDUuNzIyMjIyMjIgTDAsMCBMMzcuMTk0NDQ0NCwwIFoiPjwvcGF0aD48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iaTQiPjxwYXRoIGQ9Ik0xLjg4MzQyMjEzLDAgQzIuNjIwNzU5ODcsMCAzLjY0MzU2NDAyLDAuMTgxNjEwODcxIDMuNjQzNTY0MDIsMS4zMjExMTgxOSBMMy42NDM1NjQwMiwxLjY4OTExNTYyIEwyLjM0Mjc5MzM5LDEuNjg5MTE1NjIgTDIuMzQyNzkzMzksMS4zNjQ5MDY1OSBDMi4zNDI3OTMzOSwxLjA3OTU3NTczIDIuMTYzMTk2NjQsMC44ODkxNTMzNzEgMS44NTgxNzY4LDAuODg5MTUzMzcxIEMxLjUyOTMxNzg4LDAuODg5MTUzMzcxIDEuNDE2NDgzOTgsMS4wNzUyNzA4OCAxLjM4MDg1OTI3LDEuMjQyMTUxMDkgQzEuMzY2Nzk2ODgsMS4zMDAyNjY1NyAxLjM2MDkwNDA4LDEuNDExNzIxODQgMS4zODYxNDk0MSwxLjUxNzI1NzkzIEMxLjUzNDYwODAyLDIuMTMwNDk3MyAzLjUxMjQ0OTAyLDIuNDU2MTE4ODcgMy43MzI1NTg4MywzLjU1NTUzNzI3IEMzLjc1MzcxOTM3LDMuNjY3MDU5OCAzLjgwMDc5NDg4LDMuOTYxMTM0ODggMy43MzgwNDk4Niw0LjQxMzAwOTYzIEMzLjYxMTgyMzIxLDUuMjg5NjUyMDMgMi44MzgwNTcyLDUuNjEyMTc5NDkgMS44OTU4MTA0Myw1LjYxMjE3OTQ5IEMwLjkxNTcyOTEzNiw1LjYxMjE3OTQ5IDAsNS4yNTk5ODg5MiAwLDQuMDg4ODAwNiBMMCwzLjY4ODI0NzczIEwxLjM5OTQwODIzLDMuNjg4MjQ3NzMgTDEuNDAxMjE2MjUsNC4xOTIzMTg3OSBDMS40MDEyMTYyNSw0LjQ3ODY1ODYgMS41OTYwODA3Myw0LjY2OTI4Mjc1IDEuOTIxODU5MzIsNC42NjkyODI3NSBDMi4yNzA3NDA0LDQuNjY5MjgyNzUgMi4zODgzOTU2OSw0LjQ5MTUwNTg5IDIuNDMxMTg1NTIsNC4zMTU0MTA2MSBDMi40NTYyMjk5Niw0LjIxNjM5OTA1IDIuNDcxNDk3NjksNC4wNTQ2MzA4NSAyLjQyMTAwNzAzLDMuOTI4NjQ2NzEgQzIuMTUxMzQ0MDYsMy4yNTA5NjkxMSAwLjI5NzkyMTY3NywyLjk0MTI4ODk1IDAuMDcyMTE5OTQ3MywxLjg3MDMyMjkyIEMwLjAxNzM0MzYwODUsMS42MDU2NDE4OSAwLjAyMzgzOTA5MTIsMS4zOTkwNzYzNCAwLjA2MTc0MDU2NzcsMS4xNjQyNjAyMSBDMC4yMDAyMjE1ODEsMC4zMDg4NzMwMDcgMC45NTcxMTI3MjcsMCAxLjg4MzQyMjEzLDAgWiBNMTguODQxMTE4NiwwLjAyOTY2MzEwODkgQzE5LjU3MDQ4NzcsMC4wMjk2NjMxMDg5IDIwLjU3Nzg5MDEsMC4yMDY5NjkxMjkgMjAuNTc3ODkwMSwxLjMzNTY0NzA2IEwyMC41Nzc4OTAxLDEuNzAwNzUyMTcgTDE5LjI5MjE4NjQsMS43MDA3NTIxNyBMMTkuMjkyMTg2NCwxLjM4MDQ0NDQxIEMxOS4yOTIxODY0LDEuMDk3NDAwNSAxOS4xMTU2MDMsMC45MDg3OTQyNSAxOC44MTQ0MDAxLDAuOTA4Nzk0MjUgQzE4LjQ5MTIzMzEsMC45MDg3OTQyNSAxOC4zNzkwNjg4LDEuMDkxNjE1ODYgMTguMzQxNzcsMS4yNTkzNzA0OSBDMTguMzI5NzgzNSwxLjMxNjgxMzM0IDE4LjMyMzA4NzEsMS40MjY2NTQyOCAxOC4zNDYyNTY2LDEuNTMwMTcyNDggQzE4LjQ5NDMxMzQsMi4xMzY0MTY0NyAyMC40NTAzOTEyLDIuNDYzMTE0MjUgMjAuNjY2MDE0NCwzLjU0OTIxNDUyIEMyMC42ODk2NTI2LDMuNjU5MDU1NDcgMjAuNzM0MDQ5NiwzLjk1MDcwOTA3IDIwLjY3NTE4ODUsNC4zOTg0MTM1IEMyMC41NTE3NzQzLDUuMjY1MTAwOTMgMTkuNzgyNDk0OSw1LjU4NDgwMzMzIDE4Ljg1MTA5NjIsNS41ODQ4MDMzMyBDMTcuODc4NTgxOCw1LjU4NDgwMzMzIDE2Ljk3NTY0MjgsNS4yMzU0Mzc4MyAxNi45NzU2NDI4LDQuMDc3NTAwMzcgTDE2Ljk3NTY0MjgsMy42Nzc4ODkxOSBMMTguMzU5NTE1NCwzLjY3Nzg4OTE5IEwxOC4zNTk5MTcyLDQuMTgwNjE0OTggQzE4LjM1OTkxNzIsNC40NjMwNTM1MiAxOC41NTUxODM0LDQuNjUwNDQ5MDMgMTguODc5NzU2Nyw0LjY1MDQ0OTAzIEMxOS4yMjU3NTgzLDQuNjUwNDQ5MDMgMTkuMzQyNDc2MSw0LjQ3NTE2MDkxIDE5LjM4MjM4NjUsNC4zMDA4ODE3NCBDMTkuNDA2MzU5NSw0LjIwNTU2OTY2IDE5LjQxOTgxOTIsNC4wNDM4MDE0NiAxOS4zNzI4MTA3LDMuOTE2OTQyOSBDMTkuMTA2NDI4OSwzLjI0NzI2OTYzIDE3LjI3MTE1MzcsMi45NDAwNzgyMSAxNy4wNDc3NjI3LDEuODgxMTUyMyBDMTYuOTkwMTA2OSwxLjYxOTM2MzYgMTYuOTk5MDgwMSwxLjQxNDIxMDU4IDE3LjAzNTMwNzQsMS4xODMwOTM5MyBDMTcuMTcyMzE1MywwLjMzMzgyNzY4NiAxNy45MjI1MSwwLjAyOTY2MzEwODkgMTguODQxMTE4NiwwLjAyOTY2MzEwODkgWiBNMjMuMjM2NDg0NSwwLjE2Njg4MDIxMSBMMjMuMjM2MjI5MSw0LjExMTM3MzY2IEMyMy4yMzcyMDYzLDQuMTU3OTg0NjIgMjMuMjQwODgxOCw0LjIwNDA2NzQ1IDIzLjI0OTY3NjQsNC4yNDExNTE5NCBDMjMuMjc1MTIyNiw0LjM3MTIzOTEzIDIzLjM4Nzc1NTYsNC42MjE1OTMwOCAyMy43NDY5NDkxLDQuNjIxNTkzMDggQzI0LjExMDgzMDEsNC42MjE1OTMwOCAyNC4yMjA1ODM2LDQuMzcxMjM5MTMgMjQuMjQ4MTA1Nyw0LjI0MTE1MTk0IEMyNC4yNTk2OTA1LDQuMTg1NTI1MiAyNC4yNjE3NjYzLDQuMTA5NjUyMjIgMjQuMjU5NjkwNSw0LjA0MjExOTg4IEwyNC4yNTk2OTA1LDAuMTY2ODgwMjExIEwyNS41Nzg2MDgzLDAuMTY2ODgwMjExIEwyNS41Nzg2MDgzLDMuOTIxODUzMTIgQzI1LjU4NDMwMDIsNC4wMTg2NDQ5OSAyNS41NzQ3MjQ0LDQuMjE2Mzk5MDUgMjUuNTY3NDI1Myw0LjI2ODE5MTc4IEMyNS40NzQ3NDc1LDUuMjQ2NjcwNzkgMjQuNzA3NDc3LDUuNTY0MzU1MjkgMjMuNzQ2OTQ5MSw1LjU2NDM1NTI5IEMyMi43ODgyOTYyLDUuNTY0MzU1MjkgMjIuMDIwNTU3LDUuMjQ2NjcwNzkgMjEuOTI5NTUzMiw0LjI2ODE5MTc4IEMyMS45MjM4NjEzLDQuMjE2Mzk5MDUgMjEuOTE2Mjk0NCw0LjAxODY0NDk5IDIxLjkxNzk2ODUsMy45MjE4NTMxMiBMMjEuOTE3OTY4NSwwLjE2Njg4MDIxMSBMMjMuMjM2NDg0NSwwLjE2Njg4MDIxMSBaIE0zNC42Mjk3NjIxLDAuMDI1OTYzNjI4MiBDMzUuNTUzOTk1NiwwLjAyNTk2MzYyODIgMzYuMzYwMDM4MiwwLjMzNjg1NDUzNCAzNi40NTg3NDI3LDEuMzE5NTAzODcgQzM2LjQ2NTU3MywxLjM5MTE5NjkyIDM2LjQ2ODM4MTQsMS40NjUxMTM3OCAzNi40NjkzNjA3LDEuNTI2MTgwMjIgTDM2LjQ2OTAwNCwxLjY1NTc1NDAyIEwzNi40Njg3MjAzLDEuNjY2MTc4ODQgTDM2LjQ2ODcyMDMsMS44MzgwMzY1NCBMMzUuMTU1MzYwNSwxLjgzODAzNjU0IEwzNS4xNTUxNSwxLjUzMzU1NTU2IEMzNS4xNTQ0NDcxLDEuNDk4NzMzNjIgMzUuMTUxNDksMS40MDU2NDEyMyAzNS4xMzk0OTAxLDEuMzQ1MjY1NzEgQzM1LjExNjY1NTUsMS4yMzEzMjE3IDM1LjAxNzA4MDQsMC45NjYwMzUzMDYgMzQuNjE4MTc3NCwwLjk2NjAzNTMwNiBDMzQuMjM4MTU4MiwwLjk2NjAzNTMwNiAzNC4xMjU1OTIxLDEuMjE3NTk5OTkgMzQuMDk5Njc3MiwxLjM0NTI2NTcxIEMzNC4wODE3OTc4LDEuNDE1NDIxMzIgMzQuMDc2MTA1OSwxLjUwODExMDEyIDM0LjA3NjEwNTksMS41OTI3OTQ2IEwzNC4wNzYxMDU5LDMuOTg2ODk2NzIgQzM0LjA3NjEwNTksNC4wNTQ2MzA4NSAzNC4wODAxOTA3LDQuMTI3NjExNTEgMzQuMDg5NzY2NSw0LjE4NjEzMDU3IEMzNC4xMTI1MzQyLDQuMzI3NTE4IDM0LjI0Mzg1MDEsNC41NjgyNTMzIDM0LjYyMDk4OTksNC41NjgyNTMzIEMzNS4wMDA0MDY0LDQuNTY4MjUzMyAzNS4xMzQxMzMsNC4zMjc1MTggMzUuMTU1MzYwNSw0LjE4NjEzMDU3IEMzNS4xNjY5NDUyLDQuMTI3NjExNTEgMzUuMTcwNjI4Miw0LjA1NDYzMDg1IDM1LjE2ODk1NDEsMy45ODY4OTY3MiBMMzUuMTY4OTU0MSwzLjIyODkwNjc2IEwzNC42MzQ0NDk2LDMuMjI4OTA2NzYgTDM0LjYzNDQ0OTYsMi40NjQ5MzAzNiBMMzYuNDc5MTY2NywyLjQ2NDkzMDM2IEwzNi40NzkxNjY3LDMuODY4NTEzMzQgQzM2LjQ3NzA5MDgsMy45NjQ0MzA3OCAzNi40NzU0ODM3LDQuMDM4Njg5NDUgMzYuNDYwMDE1LDQuMjEzOTc3NTcgQzM2LjM3MjgyODIsNS4xNjk1ODcwNyAzNS41NTM5OTU2LDUuNTA4MzI0OTcgMzQuNjI2MDc5MSw1LjUwODMyNDk3IEMzMy43MDM4NTQ1LDUuNTA4MzI0OTcgMzIuODc5MzMsNS4xNjk1ODcwNyAzMi43OTM2MTY0LDQuMjEzOTc3NTcgQzMyLjc3NTkzOCw0LjAzODY4OTQ1IDMyLjc3Mjg1NzYsMy45NjQ0MzA3OCAzMi43NzI4NTc2LDMuODY4NTEzMzQgTDMyLjc3Mjg1NzYsMS42NjYxNzg4NCBDMzIuNzcyODU3NiwxLjU3MDI2MTQgMzIuNzg4NzI4LDEuNDA5MDk4NTcgMzIuNzk5MTA3NCwxLjMxOTUwMzg3IEMzMi45MTQxNTExLDAuMzM5NzQ2ODU1IDMzLjcwMzg1NDUsMC4wMjU5NjM2MjgyIDM0LjYyOTc2MjEsMC4wMjU5NjM2MjgyIFogTTEyLjE0NDc0NDcsMC4xNjY4ODAyMTEgTDEyLjgwMjI2MTYsNC4yNjE1OTk5OCBMMTMuNDYwMTgwNCwwLjE2Njg4MDIxMSBMMTUuNTg1Mjc0NiwwLjE2Njg4MDIxMSBMMTUuNzAxOTI1NSw1LjQwNTIxMDM2IEwxNC4zOTUyNjIsNS40MDUyMTAzNiBMMTQuMzU5ODM4MiwwLjU1NTkzMTA1NCBMMTMuNDYyMjU2Miw1LjQwNTIxMDM2IEwxMi4xMzk4NTYzLDUuNDA1MjEwMzYgTDExLjI0MzA3NzksMC41NTU5MzEwNTQgTDExLjIwNzc4OCw1LjQwNTIxMDM2IEw5LjkwNDQwNTgsNS40MDUyMTAzNiBMMTAuMDE3MjM5NywwLjE2Njg4MDIxMSBMMTIuMTQ0NzQ0NywwLjE2Njg4MDIxMSBaIE03LjkwMTI1MjUsMC4xNjY4ODAyMTEgTDguODYzMjUzNTgsNS40MDUyMTAzNiBMNy40NjQzMTQxLDUuNDA1MjEwMzYgTDYuNzUzODI4ODMsMC41NTU5MzEwNTQgTDYuMDI1ODY2MDIsNS40MDUyMTAzNiBMNC42MTcxNDk4Myw1LjQwNTIxMDM2IEw1LjU4MzE2ODczLDAuMTY2ODgwMjExIEw3LjkwMTI1MjUsMC4xNjY4ODAyMTEgWiBNMjguMzA0ODM2LDUuMzUwNTkyNTcgTDI3LjAyNDYyMzMsNS4zNTA1OTI1NyBMMjcuMDI0NjIzMywwLjE2Njg4MDIxMSBMMjguOTU5ODc1MywwLjE2Njg4MDIxMSBMMzAuMTg3OTkwMyw0LjM4NDM1NTQ3IEwzMC4xMTY4NzQ4LDAuMTY2ODgwMjExIEwzMS40MDU0NTgxLDAuMTY2ODgwMjExIEwzMS40MDU0NTgxLDUuMzUwNTkyNTcgTDI5LjU0OTQyNDEsNS4zNTA1OTI1NyBMMjguMjMsMC45OTkgTDI4LjMwNDgzNiw1LjM1MDU5MjU3IFoiPjwvcGF0aD48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iaTUiPjxwYXRoIGQ9Ik0yNC4zMzUyOTY2LDIuNDcwMDY0MzIgQzI1LjMwOTY1MDIsMi40NzAwNjQzMiAyNi4xMjEzMjMsMi42NTY2MDU2NCAyNi43NjkyNzY4LDMuMDI4OTczNTYgQzI3LjQxNzIzMDUsMy40MDE2OTg4MyAyNy45Mjk1MDE3LDMuOTA4NDMzNjcgMjguMzA2MDkwMiw0LjU1MDI1MDE1IEwyNi4zOTU0NTczLDUuNDgyNTk5MzcgQzI2LjE5NjA4NjksNS4xNDYzMjQ3IDI1LjkxOTE4MzYsNC44ODAwOTIzNiAyNS41NjQ3NDczLDQuNjg0NjE3MDggQzI1LjIxMDMxMTEsNC40ODkxNDE3OSAyNC44MDA0OTQyLDQuMzkxMjI1NDcgMjQuMzM1Mjk2Niw0LjM5MTIyNTQ3IEMyMy44MDM2NDIyLDQuMzkxMjI1NDcgMjMuNDEwNDM5NSw0LjQ5NDg1OTUzIDIzLjE1NTY4ODUsNC43MDIxMjc2NiBDMjIuOTAwNTkxMyw0LjkwOTM5NTc5IDIyLjc3MzU2MTksNS4xNDk1NDA5MyAyMi43NzM1NjE5LDUuNDIyMjA1NzMgQzIyLjc3MzU2MTksNS43Mzg0NjgzMSAyMi45NjE4NTYyLDUuOTY1MDMzODEgMjMuMzM4NDQ0Nyw2LjEwMDgzMDE3IEMyMy43MTQ2ODcsNi4yMzczNDEyNSAyNC4yNjg4Mzk4LDYuMzgyMDcxNTggMjQuOTk5ODY0Niw2LjUzNDY2MzgxIEMyNS4zOTg2MDU0LDYuNjExMTM4NiAyNS43OTk3NjksNi43MTE1NTY0NCAyNi4yMDQzOTQsNi44MzczNDY3NSBDMjYuNjA4NjcyOSw2Ljk2Mjc3OTcgMjYuOTc2OTU0Myw3LjEzMTgxMDQzIDI3LjMwOTIzODMsNy4zNDQ3OTYzMSBDMjcuNjQxNTIyMiw3LjU1NzQyNDgyIDI3LjkxMDExODUsNy44Mjk3MzIyNiAyOC4xMTUwMjY5LDguMTYyNzkwNyBDMjguMzE5OTM1NCw4LjQ5NTQ5MTc4IDI4LjQyMjM4OTYsOC45MTI1Mjk1NSAyOC40MjIzODk2LDkuNDE0MjYxMzcgQzI4LjQyMjM4OTYsOS43NTIzMjI4MyAyOC4zNDQ4NTY3LDEwLjEwNDMyMTMgMjguMTg5NzkwOCwxMC40Njk1NDIgQzI4LjAzNDM3ODgsMTAuODM0NzYyOCAyNy43OTM4MTkxLDExLjE3MzE4MTYgMjcuNDY3MDczMSwxMS40ODM3MjY0IEMyNy4xNDAzMjcyLDExLjc5NDk4NiAyNi43Mjc3NDEzLDEyLjA0ODM1MzQgMjYuMjI5MzE1MywxMi4yNDQ1NDM0IEMyNS43MzA4ODkzLDEyLjQ0MTA5MDggMjUuMTMyNzc4MiwxMi41MzkwMDcxIDI0LjQzNDk4MTgsMTIuNTM5MDA3MSBDMjMuMzYwNTk2OSwxMi41MzkwMDcxIDIyLjQ2ODk2ODIsMTIuMzMyODExIDIxLjc2MDA5NTcsMTEuOTIwMDYxNiBDMjEuMDUxMjIzMywxMS41MDY5NTQ4IDIwLjUwMjk1NDcsMTAuOTEwNTIyOCAyMC4xMTUyOSwxMC4xMzAwNTExIEwyMi4xOTIwNjQ5LDkuMTY1MTgyMjUgQzIyLjQyNDY2MzcsOS42MDQ3MzM2MyAyMi43NDAzMzM1LDkuOTQyNDM3NzQgMjMuMTM5MDc0MywxMC4xNzg2NTE5IEMyMy41Mzc4MTUxLDEwLjQxNDUwODggMjQuMDAzMDEyNiwxMC41MzIwNzk4IDI0LjUzNDY2NywxMC41MzIwNzk4IEMyNS4wODg0NzM2LDEwLjUzMjA3OTggMjUuNTAzODI4NiwxMC40MTc3MjUgMjUuNzgwNzMxOSwxMC4xODkwMTUzIEMyNi4wNTcyODkxLDkuOTU5OTQ4MzIgMjYuMTk2MDg2OSw5LjY4NzY0MDg4IDI2LjE5NjA4NjksOS4zNzEwMjA5NSBDMjYuMTk2MDg2OSw5LjE5NjYyOTgzIDI2LjEzMjM5OTIsOS4wNTUxMTU3MyAyNi4wMDUwMjM2LDguOTQ1NzYzOTIgQzI1Ljg3NzY0ODEsOC44MzcxMjY4MyAyNS43MTE1MDYxLDguNzQxMzU0NjYgMjUuNTA2NTk3Niw4LjY1OTg3Njg1IEMyNS4zMDEzNDMxLDguNTc4MDQxNjcgMjUuMDYwNzgzMyw4LjUxMDE0MzQ5IDI0Ljc4Mzg4LDguNDU1MTEwMjMgQzI0LjUwNjk3NjcsOC40MDA3OTE2OSAyNC4yMTg5OTcyLDguMzQwNzU1NCAyMy45MTk5NDE2LDguMjc1MzU4NzMgQzIzLjQ5ODcwMjUsOC4xODgxNjMxOCAyMy4wODY0NjI2LDguMDg0ODg2NDcgMjIuNjgyMTgzOCw3Ljk2NDgxMzkgQzIyLjI3NzkwNSw3Ljg0NTA5ODY5IDIxLjkxNTE2MTYsNy42Nzg1Njk0NyAyMS41OTM5NTM4LDcuNDY2Mjk4MzEgQzIxLjI3Mjc0NTksNy4yNTMzMTI0NCAyMS4wMTI0NTY4LDYuOTgwNjQ3NjQgMjAuODEzMDg2NCw2LjY0ODMwMzkyIEMyMC42MTM3MTYsNi4zMTU5NjAyIDIwLjUxNDAzMDgsNS44OTg5MjI0MyAyMC41MTQwMzA4LDUuMzk3NTQ3OTcgQzIwLjUxNDAzMDgsNS4wMTU1MzEzNyAyMC42MDU0MDg5LDQuNjQ3ODA5MTIgMjAuNzg4MTY1MSw0LjI5MzY2NjUgQzIwLjk3MDkyMTMsMy45MzkxNjY1MyAyMS4yMjg0NDE0LDMuNjI2MTIwMTggMjEuNTYwNzI1NCwzLjM1MzA5ODAzIEMyMS44OTMwMDkzLDMuMDgwNzkwNTkgMjIuMjk0MTczLDIuODY1NjYwNTYgMjIuNzY1MjU0OCwyLjcwNzM1MDYgQzIzLjIzNTk5MDQsMi41NDkwNDA2MyAyMy43NTkzMzc3LDIuNDcwMDY0MzIgMjQuMzM1Mjk2NiwyLjQ3MDA2NDMyIFogTTMzLjEwNzM1MTUsMi40NzAwNjQzMiBDMzQuMDgxNzA1LDIuNDcwMDY0MzIgMzQuODkzMzc3OSwyLjY1NjYwNTY0IDM1LjU0MTMzMTYsMy4wMjg5NzM1NiBDMzYuMTg5Mjg1NCwzLjQwMTY5ODgzIDM2LjcwMTU1NjUsMy45MDg0MzM2NyAzNy4wNzgxNDUxLDQuNTUwMjUwMTUgTDM1LjE2NzUxMjIsNS40ODI1OTkzNyBDMzQuOTY4MTQxOCw1LjE0NjMyNDcgMzQuNjkxMjM4NCw0Ljg4MDA5MjM2IDM0LjMzNjgwMjIsNC42ODQ2MTcwOCBDMzMuOTgyMzY1OSw0LjQ4OTE0MTc5IDMzLjU3MjU0OSw0LjM5MTIyNTQ3IDMzLjEwNzM1MTUsNC4zOTEyMjU0NyBDMzIuNTc1Njk3MSw0LjM5MTIyNTQ3IDMyLjE4MjQ5NDQsNC40OTQ4NTk1MyAzMS45Mjc3NDMzLDQuNzAyMTI3NjYgQzMxLjY3MjY0NjEsNC45MDkzOTU3OSAzMS41NDU2MTY3LDUuMTQ5NTQwOTMgMzEuNTQ1NjE2Nyw1LjQyMjIwNTczIEMzMS41NDU2MTY3LDUuNzM4NDY4MzEgMzEuNzMzOTExLDUuOTY1MDMzODEgMzIuMTEwNDk5NSw2LjEwMDgzMDE3IEMzMi40ODY3NDE5LDYuMjM3MzQxMjUgMzMuMDQwODk0Nyw2LjM4MjA3MTU4IDMzLjc3MTkxOTQsNi41MzQ2NjM4MSBDMzQuMTcwNjYwMiw2LjYxMTEzODYgMzQuNTcxODIzOSw2LjcxMTU1NjQ0IDM0Ljk3NjQ0ODksNi44MzczNDY3NSBDMzUuMzgwNzI3Nyw2Ljk2Mjc3OTcgMzUuNzQ5MDA5MSw3LjEzMTgxMDQzIDM2LjA4MTI5MzEsNy4zNDQ3OTYzMSBDMzYuNDEzNTc3MSw3LjU1NzQyNDgyIDM2LjY4MjE3MzMsNy44Mjk3MzIyNiAzNi44ODcwODE4LDguMTYyNzkwNyBDMzcuMDkxOTkwMiw4LjQ5NTQ5MTc4IDM3LjE5NDQ0NDQsOC45MTI1Mjk1NSAzNy4xOTQ0NDQ0LDkuNDE0MjYxMzcgQzM3LjE5NDQ0NDQsOS43NTIzMjI4MyAzNy4xMTY5MTE1LDEwLjEwNDMyMTMgMzYuOTYxODQ1NywxMC40Njk1NDIgQzM2LjgwNjQzMzcsMTAuODM0NzYyOCAzNi41NjU4NzM5LDExLjE3MzE4MTYgMzYuMjM5MTI4LDExLjQ4MzcyNjQgQzM1LjkxMjM4MjEsMTEuNzk0OTg2IDM1LjQ5OTc5NjEsMTIuMDQ4MzUzNCAzNS4wMDEzNzAyLDEyLjI0NDU0MzQgQzM0LjUwMjk0NDIsMTIuNDQxMDkwOCAzMy45MDQ4MzMsMTIuNTM5MDA3MSAzMy4yMDcwMzY3LDEyLjUzOTAwNzEgQzMyLjEzMjY1MTgsMTIuNTM5MDA3MSAzMS4yNDEwMjMxLDEyLjMzMjgxMSAzMC41MzIxNTA2LDExLjkyMDA2MTYgQzI5LjgyMzI3ODEsMTEuNTA2OTU0OCAyOS4yNzUwMDk1LDEwLjkxMDUyMjggMjguODg3MzQ0OSwxMC4xMzAwNTExIEwzMC45NjQxMTk4LDkuMTY1MTgyMjUgQzMxLjE5NjcxODYsOS42MDQ3MzM2MyAzMS41MTIzODgzLDkuOTQyNDM3NzQgMzEuOTExMTI5MSwxMC4xNzg2NTE5IEMzMi4zMDk4Njk5LDEwLjQxNDUwODggMzIuNzc1MDY3NSwxMC41MzIwNzk4IDMzLjMwNjcyMTgsMTAuNTMyMDc5OCBDMzMuODYwNTI4NSwxMC41MzIwNzk4IDM0LjI3NTg4MzUsMTAuNDE3NzI1IDM0LjU1Mjc4NjgsMTAuMTg5MDE1MyBDMzQuODI5MzQ0LDkuOTU5OTQ4MzIgMzQuOTY4MTQxOCw5LjY4NzY0MDg4IDM0Ljk2ODE0MTgsOS4zNzEwMjA5NSBDMzQuOTY4MTQxOCw5LjE5NjYyOTgzIDM0LjkwNDQ1NCw5LjA1NTExNTczIDM0Ljc3NzA3ODUsOC45NDU3NjM5MiBDMzQuNjQ5NzAyOSw4LjgzNzEyNjgzIDM0LjQ4MzU2MSw4Ljc0MTM1NDY2IDM0LjI3ODY1MjUsOC42NTk4NzY4NSBDMzQuMDczMzk3OSw4LjU3ODA0MTY3IDMzLjgzMjgzODIsOC41MTAxNDM0OSAzMy41NTU5MzQ4LDguNDU1MTEwMjMgQzMzLjI3OTAzMTUsOC40MDA3OTE2OSAzMi45OTEwNTIxLDguMzQwNzU1NCAzMi42OTE5OTY1LDguMjc1MzU4NzMgQzMyLjI3MDc1NzMsOC4xODgxNjMxOCAzMS44NTg1MTc1LDguMDg0ODg2NDcgMzEuNDU0MjM4Niw3Ljk2NDgxMzkgQzMxLjA0OTk1OTgsNy44NDUwOTg2OSAzMC42ODcyMTY1LDcuNjc4NTY5NDcgMzAuMzY2MDA4Niw3LjQ2NjI5ODMxIEMzMC4wNDQ4MDA4LDcuMjUzMzEyNDQgMjkuNzg0NTExNiw2Ljk4MDY0NzY0IDI5LjU4NTE0MTIsNi42NDgzMDM5MiBDMjkuMzg1NzcwOSw2LjMxNTk2MDIgMjkuMjg2MDg1Nyw1Ljg5ODkyMjQzIDI5LjI4NjA4NTcsNS4zOTc1NDc5NyBDMjkuMjg2MDg1Nyw1LjAxNTUzMTM3IDI5LjM3NzQ2MzgsNC42NDc4MDkxMiAyOS41NjAyMTk5LDQuMjkzNjY2NSBDMjkuNzQyOTc2MSwzLjkzOTE2NjUzIDMwLjAwMDQ5NjIsMy42MjYxMjAxOCAzMC4zMzI3ODAyLDMuMzUzMDk4MDMgQzMwLjY2NTA2NDIsMy4wODA3OTA1OSAzMS4wNjYyMjc5LDIuODY1NjYwNTYgMzEuNTM3MzA5NiwyLjcwNzM1MDYgQzMyLjAwODA0NTMsMi41NDkwNDA2MyAzMi41MzEzOTI2LDIuNDcwMDY0MzIgMzMuMTA3MzUxNSwyLjQ3MDA2NDMyIFogTTEzLjc3MzIwNTcsMi40NzAwMjg1OSBDMTQuNDE1NjIxNCwyLjQ3MDAyODU5IDE1LjAwODE5NDUsMi41OTAxMDExNiAxNS41NTA5MjUsMi44Mjk1MzE1OSBDMTYuMDkzMzA5NCwzLjA2OTY3NjczIDE2LjU0MjIzODksMy4zOTY2NjAwNyAxNi44OTY2NzUxLDMuODEwODM4OTcgTDE2Ljg5NjY3NTEsMi40ODcxODE4MSBMMTkuMTM5NTkyLDIuNDg3MTgxODEgTDE5LjEzOTU5MiwxMi41MjE4MTgxIEwxNi44OTY2NzUxLDEyLjUyMTgxODEgTDE2Ljg5NjY3NTEsMTEuMDk1OTU2MyBDMTYuNTQyMjM4OSwxMS41NDQwODQzIDE2LjA4ODExNzQsMTEuODk2Nzk3NSAxNS41MzQzMTA4LDEyLjE1MzczODUgQzE0Ljk4MDUwNDIsMTIuNDEwNjc5NSAxNC4zODIzOTMsMTIuNTM4OTcxNCAxMy43Mzk5NzczLDEyLjUzODk3MTQgQzEzLjE1Mjk0MjMsMTIuNTM4OTcxNCAxMi41NzQyMTQzLDEyLjQyNjQwMzMgMTIuMDAzNzkzNSwxMi4yMDA1NTI1IEMxMS40MzMzNzI2LDExLjk3NDM0NDQgMTAuOTIxMTAxNSwxMS42NDYyODkgMTAuNDY2OTgwMSwxMS4yMTYzODYzIEMxMC4wMTI4NTg2LDEwLjc4NjQ4MzYgOS42NDcwMDAxMSwxMC4yNjAwOTQgOS4zNzA0NDI5Miw5LjYzNzU3NDkxIEM5LjA5MzUzOTYsOS4wMTQ2OTg0NCA4Ljk1NTA4Nzk0LDguMzA2NDEzMjIgOC45NTUwODc5NCw3LjUxMzA3NjU4IEM4Ljk1NTA4Nzk0LDYuNzA4MzA0NDcgOS4wOTA0MjQ0NCw1Ljk5NTAxNjIyIDkuMzYyMTM1ODIsNS4zNzE3ODI0IEM5LjYzMzUwMTA3LDQuNzQ4OTA1OTMgOS45OTM0NzUzOSw0LjIyMjg3MzcxIDEwLjQ0MjA1ODgsMy43OTI5NzEwMyBDMTAuODkwNjQyMSwzLjM2MjcxMDk4IDExLjQwNTY4MjMsMy4wMzUwMTI5MiAxMS45ODcxNzkzLDIuODA5NTE5NDkgQzEyLjU2ODY3NjMsMi41ODMzMTEzNCAxMy4xNjQwMTg0LDIuNDcwMDI4NTkgMTMuNzczMjA1NywyLjQ3MDAyODU5IFogTTQuMTUzNTQ5NzgsMCBDNC43ODQ4ODkzNSwwIDUuMzY2Mzg2MzIsMC4xMTcyMTM3MDEgNS44OTgwNDA2OSwwLjM1MTk5ODQ2MSBDNi40Mjk2OTUwNiwwLjU4NjQyNTg2MiA2Ljg4OTAwODQ0LDAuOTAzNDAzMTU2IDcuMjc3MDE5MjIsMS4zMDQwMDI0MiBDNy42NjQ2ODM4NiwxLjcwNDI0NDMyIDcuOTY4OTMxMzgsMi4xNzU5NTggOC4xOTA4MDAxNywyLjcxOTE0MzQ0IEM4LjQxMjMyMjgyLDMuMjYxOTcxNTIgOC41MjMwODQxNSwzLjg0MjMyMjI4IDguNTIzMDg0MTUsNC40NjAxOTU3MiBDOC41MjMwODQxNSw1LjA3NzM1NDQ0IDguNDEyMzIyODIsNS42NjA1NjQwOCA4LjE5MDgwMDE3LDYuMjA5NDY3MjYgQzcuOTY4OTMxMzgsNi43NTgzNzA0NCA3LjY2NDY4Mzg2LDcuMjMyOTQyOTkgNy4yNzcwMTkyMiw3LjYzMzU0MjI1IEM2Ljg4OTAwODQ0LDguMDMzNzg0MTYgNi40MjY5MjYwMyw4LjM1MTExODgxIDUuODg5NzMzNTksOC41ODUxODg4NSBDNS4zNTIxOTUwMiw4LjgxOTYxNjI1IDQuNzY4Mjc1MTUsOC45MzY4Mjk5NSA0LjEzNjkzNTU4LDguOTM2ODI5OTUgTDIuMjU5NTMxMDgsOC45MzY4Mjk5NSBMMi4yNTk1MzEwOCwxMi41MjE4NTM5IEwwLDEyLjUyMTg1MzkgTDAsMCBMNC4xNTM1NDk3OCwwIFogTTE0LjEwNTQ4OTcsNC41ODAyMzI1NiBDMTMuNjk1NjcyOCw0LjU4MDIzMjU2IDEzLjMxMDc3NzEsNC42NTU2MzUyNyAxMi45NTA4MDI4LDQuODA1NzI1OTkgQzEyLjU5MDgyODUsNC45NTU4MTY3IDEyLjI3NzkyNzgsNS4xNjAyMjU5NiAxMi4wMTIxMDA2LDUuNDE3NTI0MzMgQzExLjc0NjI3MzQsNS42NzU1Mzc0MSAxMS41Mzg1OTU5LDUuOTgxNzkzOTQgMTEuMzg5MDY4MSw2LjMzNjI5MzkxIEMxMS4yMzk1NDAzLDYuNjkwNzkzODkgMTEuMTY0Nzc2NCw3LjA3MTczODQxIDExLjE2NDc3NjQsNy40ODAxOTk1NyBDMTEuMTY0Nzc2NCw3Ljg4ODMwMzM3IDExLjIzOTU0MDMsOC4yNzI0NjQxMyAxMS4zODkwNjgxLDguNjMxOTY3MTIgQzExLjUzODU5NTksOC45OTE0NzAxMiAxMS43NDYyNzM0LDkuMzAyNzI5NjcgMTIuMDEyMTAwNiw5LjU2NjQ2MDUgQzEyLjI3NzkyNzgsOS44Mjk0NzY2MSAxMi41OTA4Mjg1LDEwLjAzNjAzIDEyLjk1MDgwMjgsMTAuMTg2ODM1NCBDMTMuMzEwNzc3MSwxMC4zMzY5MjYyIDEzLjY5NTY3MjgsMTAuNDExOTcxNSAxNC4xMDU0ODk3LDEwLjQxMTk3MTUgQzE0LjUyNjM4MjcsMTAuNDExOTcxNSAxNC45MTM3MDEyLDEwLjMzNjkyNjIgMTUuMjY4NDgzNiwxMC4xODY4MzU0IEMxNS42MjI5MTk5LDEwLjAzNjAzIDE1LjkyNzE2NzQsOS44MjY2MTc3NCAxNi4xODIyNjQ2LDkuNTU3ODgzODkgQzE2LjQzNzAxNTYsOS4yODk1MDczOSAxNi42MzkxNTUsOC45Nzc4OTA0OCAxNi43ODg2ODI4LDguNjIzNzQ3ODcgQzE2LjkzODIxMDYsOC4yNjk2MDUyNiAxNy4wMTI5NzQ1LDcuODg4MzAzMzcgMTcuMDEyOTc0NSw3LjQ4MDE5OTU3IEMxNy4wMTI5NzQ1LDcuMDgyODE2NTQgMTYuOTM4MjEwNiw2LjcwNjg3NTAzIDE2Ljc4ODY4MjgsNi4zNTIzNzUwNiBDMTYuNjM5MTU1LDUuOTk3ODc1MDkgMTYuNDM3MDE1Niw1LjY4OTExNzA1IDE2LjE4MjI2NDYsNS40MjYxMDA5NCBDMTUuOTI3MTY3NCw1LjE2MjcyNzQ3IDE1LjYyMjkxOTksNC45NTU4MTY3IDE1LjI2ODQ4MzYsNC44MDU3MjU5OSBDMTQuOTEzNzAxMiw0LjY1NTYzNTI3IDE0LjUyNjM4MjcsNC41ODAyMzI1NiAxNC4xMDU0ODk3LDQuNTgwMjMyNTYgWiBNMy45ODc0MDc3OSwyLjE2MTMwNjI4IEwyLjI1OTUzMTA4LDIuMTYxMzA2MjggTDIuMjU5NTMxMDgsNi43NzU1MjM2NyBMMy45ODc0MDc3OSw2Ljc3NTUyMzY3IEM0LjMxOTY5MTc3LDYuNzc1NTIzNjcgNC42MjQyODU0Miw2LjcxNTQ4NzM4IDQuOTAxMTg4NzQsNi41OTU0MTQ4MSBDNS4xNzc3NDU5Myw2LjQ3NTM0MjI0IDUuNDE2MjI4OTIsNi4zMDk1Mjc3NCA1LjYxNTU5OTMsNi4wOTgzMjg2NiBDNS44MTQ5Njk2OSw1Ljg4Njc3MjIyIDUuOTcwMDM1NTUsNS42NDA1NTE5OCA2LjA4MDc5Njg4LDUuMzYwMzgyNjUgQzYuMTkxMjEyMDgsNS4wODA1NzA2NyA2LjI0NjkzODg3LDQuNzgwMDMxODkgNi4yNDY5Mzg4Nyw0LjQ2MDE5NTcyIEM2LjI0NjkzODg3LDQuMTM5NjQ0ODQgNi4xOTEyMTIwOCwzLjgzOTQ2MzQxIDYuMDgwNzk2ODgsMy41NTkyOTQwOCBDNS45NzAwMzU1NSwzLjI3OTEyNDc1IDUuODE0OTY5NjksMy4wMzYxMjA3MyA1LjYxNTU5OTMsMi44MzAyODIwNCBDNS40MTYyMjg5MiwyLjYyNDQ0MzM0IDUuMTc3NzQ1OTMsMi40NjE0ODc3MSA0LjkwMTE4ODc0LDIuMzQxNDE1MTQgQzQuNjI0Mjg1NDIsMi4yMjEzNDI1NyA0LjMxOTY5MTc3LDIuMTYxMzA2MjggMy45ODc0MDc3OSwyLjE2MTMwNjI4IFoiPjwvcGF0aD48L2NsaXBQYXRoPjwvZGVmcz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjMzLjAgLTE2MC4wKSI+PGcgY2xpcC1wYXRoPSJ1cmwoI2kwKSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjMzLjAgMTYwLjApIj48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMC4wMDAxODA1NTU1NTU1NTIwNzU0OCAwLjApIj48ZyBjbGlwLXBhdGg9InVybCgjaTEpIj48cG9seWdvbiBwb2ludHM9IjAsMCA1MiwwIDUyLDUyIDAsNTIgMCwwIiBzdHJva2U9Im5vbmUiIGZpbGw9InVybCgjaTIpIj48L3BvbHlnb24+PC9nPjwvZz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3LjU4MzMzMzMzMzMzMzMzMiAxNC44MDU1NTU1NTU1NTU1NSkiPjxnIGNsaXAtcGF0aD0idXJsKCNpMykiPjxnIGNsaXAtcGF0aD0idXJsKCNpNCkiPjxwb2x5Z29uIHBvaW50cz0iMCwwIDM2LjQ3OTE2NjcsMCAzNi40NzkxNjY3LDUuNjEyMTc5NDkgMCw1LjYxMjE3OTQ5IDAsMCIgc3Ryb2tlPSJub25lIiBmaWxsPSIjRkZGRkZGIj48L3BvbHlnb24+PC9nPjwvZz48L2c+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNy41ODMzMzMzMzMzMzMzNzEgMjQuNDMyMDgwMjU1NDc3MzMpIj48ZyBjbGlwLXBhdGg9InVybCgjaTUpIj48cG9seWdvbiBwb2ludHM9IjAsMCAzNy4xOTQ0NDQ0LDAgMzcuMTk0NDQ0NCwxMi41MzkwMDcxIDAsMTIuNTM5MDA3MSAwLDAiIHN0cm9rZT0ibm9uZSIgZmlsbD0iI0ZGRkZGRiI+PC9wb2x5Z29uPjwvZz48L2c+PC9nPjwvZz48L2c+PC9zdmc+", + "icon_light": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgd2lkdGg9IjUycHgiIGhlaWdodD0iNTJweCIgdmlld0JveD0iMCAwIDUyLjAgNTIuMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PGNsaXBQYXRoIGlkPSJpMCI+PHBhdGggZD0iTTM2MCwwIEwzNjAsODAwIEwwLDgwMCBMMCwwIEwzNjAsMCBaIj48L3BhdGg+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImkxIj48cGF0aCBkPSJNMjYsMCBDMzMuOTkxMDI3OCwwIDQxLjEzOTU4MzMsMC45NzUgNDUuOTA4Nzc3OCw1Ljc3Nzc3Nzc4IEM0OS43MTAxOTQ0LDkuNjA1OTE2NjcgNTIsMTUuODY1MDU1NiA1MiwyNiBDNTIsMzYuMTM0OTQ0NCA0OS43MDk4MzMzLDQyLjM5NDQ0NDQgNDUuOTA4MDU1Niw0Ni4yMjI1ODMzIEM0MS4xMzg4NjExLDUxLjAyNDYzODkgMzMuOTkwMzA1Niw1MiAyNiw1MiBDMTguMDA4OTcyMiw1MiAxMC44NjA3Nzc4LDUxLjAyNDYzODkgNi4wOTE1ODMzMyw0Ni4yMjI1ODMzIEMyLjI5MDE2NjY3LDQyLjM5NDQ0NDQgMCwzNi4xMzQ5NDQ0IDAsMjYgQzAsMTUuODY1MDU1NiAyLjI4OTgwNTU2LDkuNjA1NTU1NTYgNi4wOTA4NjExMSw1Ljc3Nzc3Nzc4IEMxMC44NjAwNTU2LDAuOTc1IDE4LjAwODYxMTEsMCAyNiwwIFoiPjwvcGF0aD48L2NsaXBQYXRoPjxsaW5lYXJHcmFkaWVudCBpZD0iaTIiIHgxPSIyNnB4IiB5MT0iNTJweCIgeDI9IjI2cHgiIHkyPSIwLjE5NTkyMTE0OHB4IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agc3RvcC1jb2xvcj0iIzI5MjlCMiIgb2Zmc2V0PSIwJSI+PC9zdG9wPjxzdG9wIHN0b3AtY29sb3I9IiMxQTQwQ0MiIG9mZnNldD0iMTAwJSI+PC9zdG9wPjwvbGluZWFyR3JhZGllbnQ+PGNsaXBQYXRoIGlkPSJpMyI+PHBhdGggZD0iTTM3LjE5NDQ0NDQsMCBMMzcuMTk0NDQ0NCw1LjcyMjIyMjIyIEwwLDUuNzIyMjIyMjIgTDAsMCBMMzcuMTk0NDQ0NCwwIFoiPjwvcGF0aD48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iaTQiPjxwYXRoIGQ9Ik0xLjg4MzQyMjEzLDAgQzIuNjIwNzU5ODcsMCAzLjY0MzU2NDAyLDAuMTgxNjEwODcxIDMuNjQzNTY0MDIsMS4zMjExMTgxOSBMMy42NDM1NjQwMiwxLjY4OTExNTYyIEwyLjM0Mjc5MzM5LDEuNjg5MTE1NjIgTDIuMzQyNzkzMzksMS4zNjQ5MDY1OSBDMi4zNDI3OTMzOSwxLjA3OTU3NTczIDIuMTYzMTk2NjQsMC44ODkxNTMzNzEgMS44NTgxNzY4LDAuODg5MTUzMzcxIEMxLjUyOTMxNzg4LDAuODg5MTUzMzcxIDEuNDE2NDgzOTgsMS4wNzUyNzA4OCAxLjM4MDg1OTI3LDEuMjQyMTUxMDkgQzEuMzY2Nzk2ODgsMS4zMDAyNjY1NyAxLjM2MDkwNDA4LDEuNDExNzIxODQgMS4zODYxNDk0MSwxLjUxNzI1NzkzIEMxLjUzNDYwODAyLDIuMTMwNDk3MyAzLjUxMjQ0OTAyLDIuNDU2MTE4ODcgMy43MzI1NTg4MywzLjU1NTUzNzI3IEMzLjc1MzcxOTM3LDMuNjY3MDU5OCAzLjgwMDc5NDg4LDMuOTYxMTM0ODggMy43MzgwNDk4Niw0LjQxMzAwOTYzIEMzLjYxMTgyMzIxLDUuMjg5NjUyMDMgMi44MzgwNTcyLDUuNjEyMTc5NDkgMS44OTU4MTA0Myw1LjYxMjE3OTQ5IEMwLjkxNTcyOTEzNiw1LjYxMjE3OTQ5IDAsNS4yNTk5ODg5MiAwLDQuMDg4ODAwNiBMMCwzLjY4ODI0NzczIEwxLjM5OTQwODIzLDMuNjg4MjQ3NzMgTDEuNDAxMjE2MjUsNC4xOTIzMTg3OSBDMS40MDEyMTYyNSw0LjQ3ODY1ODYgMS41OTYwODA3Myw0LjY2OTI4Mjc1IDEuOTIxODU5MzIsNC42NjkyODI3NSBDMi4yNzA3NDA0LDQuNjY5MjgyNzUgMi4zODgzOTU2OSw0LjQ5MTUwNTg5IDIuNDMxMTg1NTIsNC4zMTU0MTA2MSBDMi40NTYyMjk5Niw0LjIxNjM5OTA1IDIuNDcxNDk3NjksNC4wNTQ2MzA4NSAyLjQyMTAwNzAzLDMuOTI4NjQ2NzEgQzIuMTUxMzQ0MDYsMy4yNTA5NjkxMSAwLjI5NzkyMTY3NywyLjk0MTI4ODk1IDAuMDcyMTE5OTQ3MywxLjg3MDMyMjkyIEMwLjAxNzM0MzYwODUsMS42MDU2NDE4OSAwLjAyMzgzOTA5MTIsMS4zOTkwNzYzNCAwLjA2MTc0MDU2NzcsMS4xNjQyNjAyMSBDMC4yMDAyMjE1ODEsMC4zMDg4NzMwMDcgMC45NTcxMTI3MjcsMCAxLjg4MzQyMjEzLDAgWiBNMTguODQxMTE4NiwwLjAyOTY2MzEwODkgQzE5LjU3MDQ4NzcsMC4wMjk2NjMxMDg5IDIwLjU3Nzg5MDEsMC4yMDY5NjkxMjkgMjAuNTc3ODkwMSwxLjMzNTY0NzA2IEwyMC41Nzc4OTAxLDEuNzAwNzUyMTcgTDE5LjI5MjE4NjQsMS43MDA3NTIxNyBMMTkuMjkyMTg2NCwxLjM4MDQ0NDQxIEMxOS4yOTIxODY0LDEuMDk3NDAwNSAxOS4xMTU2MDMsMC45MDg3OTQyNSAxOC44MTQ0MDAxLDAuOTA4Nzk0MjUgQzE4LjQ5MTIzMzEsMC45MDg3OTQyNSAxOC4zNzkwNjg4LDEuMDkxNjE1ODYgMTguMzQxNzcsMS4yNTkzNzA0OSBDMTguMzI5NzgzNSwxLjMxNjgxMzM0IDE4LjMyMzA4NzEsMS40MjY2NTQyOCAxOC4zNDYyNTY2LDEuNTMwMTcyNDggQzE4LjQ5NDMxMzQsMi4xMzY0MTY0NyAyMC40NTAzOTEyLDIuNDYzMTE0MjUgMjAuNjY2MDE0NCwzLjU0OTIxNDUyIEMyMC42ODk2NTI2LDMuNjU5MDU1NDcgMjAuNzM0MDQ5NiwzLjk1MDcwOTA3IDIwLjY3NTE4ODUsNC4zOTg0MTM1IEMyMC41NTE3NzQzLDUuMjY1MTAwOTMgMTkuNzgyNDk0OSw1LjU4NDgwMzMzIDE4Ljg1MTA5NjIsNS41ODQ4MDMzMyBDMTcuODc4NTgxOCw1LjU4NDgwMzMzIDE2Ljk3NTY0MjgsNS4yMzU0Mzc4MyAxNi45NzU2NDI4LDQuMDc3NTAwMzcgTDE2Ljk3NTY0MjgsMy42Nzc4ODkxOSBMMTguMzU5NTE1NCwzLjY3Nzg4OTE5IEwxOC4zNTk5MTcyLDQuMTgwNjE0OTggQzE4LjM1OTkxNzIsNC40NjMwNTM1MiAxOC41NTUxODM0LDQuNjUwNDQ5MDMgMTguODc5NzU2Nyw0LjY1MDQ0OTAzIEMxOS4yMjU3NTgzLDQuNjUwNDQ5MDMgMTkuMzQyNDc2MSw0LjQ3NTE2MDkxIDE5LjM4MjM4NjUsNC4zMDA4ODE3NCBDMTkuNDA2MzU5NSw0LjIwNTU2OTY2IDE5LjQxOTgxOTIsNC4wNDM4MDE0NiAxOS4zNzI4MTA3LDMuOTE2OTQyOSBDMTkuMTA2NDI4OSwzLjI0NzI2OTYzIDE3LjI3MTE1MzcsMi45NDAwNzgyMSAxNy4wNDc3NjI3LDEuODgxMTUyMyBDMTYuOTkwMTA2OSwxLjYxOTM2MzYgMTYuOTk5MDgwMSwxLjQxNDIxMDU4IDE3LjAzNTMwNzQsMS4xODMwOTM5MyBDMTcuMTcyMzE1MywwLjMzMzgyNzY4NiAxNy45MjI1MSwwLjAyOTY2MzEwODkgMTguODQxMTE4NiwwLjAyOTY2MzEwODkgWiBNMjMuMjM2NDg0NSwwLjE2Njg4MDIxMSBMMjMuMjM2MjI5MSw0LjExMTM3MzY2IEMyMy4yMzcyMDYzLDQuMTU3OTg0NjIgMjMuMjQwODgxOCw0LjIwNDA2NzQ1IDIzLjI0OTY3NjQsNC4yNDExNTE5NCBDMjMuMjc1MTIyNiw0LjM3MTIzOTEzIDIzLjM4Nzc1NTYsNC42MjE1OTMwOCAyMy43NDY5NDkxLDQuNjIxNTkzMDggQzI0LjExMDgzMDEsNC42MjE1OTMwOCAyNC4yMjA1ODM2LDQuMzcxMjM5MTMgMjQuMjQ4MTA1Nyw0LjI0MTE1MTk0IEMyNC4yNTk2OTA1LDQuMTg1NTI1MiAyNC4yNjE3NjYzLDQuMTA5NjUyMjIgMjQuMjU5NjkwNSw0LjA0MjExOTg4IEwyNC4yNTk2OTA1LDAuMTY2ODgwMjExIEwyNS41Nzg2MDgzLDAuMTY2ODgwMjExIEwyNS41Nzg2MDgzLDMuOTIxODUzMTIgQzI1LjU4NDMwMDIsNC4wMTg2NDQ5OSAyNS41NzQ3MjQ0LDQuMjE2Mzk5MDUgMjUuNTY3NDI1Myw0LjI2ODE5MTc4IEMyNS40NzQ3NDc1LDUuMjQ2NjcwNzkgMjQuNzA3NDc3LDUuNTY0MzU1MjkgMjMuNzQ2OTQ5MSw1LjU2NDM1NTI5IEMyMi43ODgyOTYyLDUuNTY0MzU1MjkgMjIuMDIwNTU3LDUuMjQ2NjcwNzkgMjEuOTI5NTUzMiw0LjI2ODE5MTc4IEMyMS45MjM4NjEzLDQuMjE2Mzk5MDUgMjEuOTE2Mjk0NCw0LjAxODY0NDk5IDIxLjkxNzk2ODUsMy45MjE4NTMxMiBMMjEuOTE3OTY4NSwwLjE2Njg4MDIxMSBMMjMuMjM2NDg0NSwwLjE2Njg4MDIxMSBaIE0zNC42Mjk3NjIxLDAuMDI1OTYzNjI4MiBDMzUuNTUzOTk1NiwwLjAyNTk2MzYyODIgMzYuMzYwMDM4MiwwLjMzNjg1NDUzNCAzNi40NTg3NDI3LDEuMzE5NTAzODcgQzM2LjQ2NTU3MywxLjM5MTE5NjkyIDM2LjQ2ODM4MTQsMS40NjUxMTM3OCAzNi40NjkzNjA3LDEuNTI2MTgwMjIgTDM2LjQ2OTAwNCwxLjY1NTc1NDAyIEwzNi40Njg3MjAzLDEuNjY2MTc4ODQgTDM2LjQ2ODcyMDMsMS44MzgwMzY1NCBMMzUuMTU1MzYwNSwxLjgzODAzNjU0IEwzNS4xNTUxNSwxLjUzMzU1NTU2IEMzNS4xNTQ0NDcxLDEuNDk4NzMzNjIgMzUuMTUxNDksMS40MDU2NDEyMyAzNS4xMzk0OTAxLDEuMzQ1MjY1NzEgQzM1LjExNjY1NTUsMS4yMzEzMjE3IDM1LjAxNzA4MDQsMC45NjYwMzUzMDYgMzQuNjE4MTc3NCwwLjk2NjAzNTMwNiBDMzQuMjM4MTU4MiwwLjk2NjAzNTMwNiAzNC4xMjU1OTIxLDEuMjE3NTk5OTkgMzQuMDk5Njc3MiwxLjM0NTI2NTcxIEMzNC4wODE3OTc4LDEuNDE1NDIxMzIgMzQuMDc2MTA1OSwxLjUwODExMDEyIDM0LjA3NjEwNTksMS41OTI3OTQ2IEwzNC4wNzYxMDU5LDMuOTg2ODk2NzIgQzM0LjA3NjEwNTksNC4wNTQ2MzA4NSAzNC4wODAxOTA3LDQuMTI3NjExNTEgMzQuMDg5NzY2NSw0LjE4NjEzMDU3IEMzNC4xMTI1MzQyLDQuMzI3NTE4IDM0LjI0Mzg1MDEsNC41NjgyNTMzIDM0LjYyMDk4OTksNC41NjgyNTMzIEMzNS4wMDA0MDY0LDQuNTY4MjUzMyAzNS4xMzQxMzMsNC4zMjc1MTggMzUuMTU1MzYwNSw0LjE4NjEzMDU3IEMzNS4xNjY5NDUyLDQuMTI3NjExNTEgMzUuMTcwNjI4Miw0LjA1NDYzMDg1IDM1LjE2ODk1NDEsMy45ODY4OTY3MiBMMzUuMTY4OTU0MSwzLjIyODkwNjc2IEwzNC42MzQ0NDk2LDMuMjI4OTA2NzYgTDM0LjYzNDQ0OTYsMi40NjQ5MzAzNiBMMzYuNDc5MTY2NywyLjQ2NDkzMDM2IEwzNi40NzkxNjY3LDMuODY4NTEzMzQgQzM2LjQ3NzA5MDgsMy45NjQ0MzA3OCAzNi40NzU0ODM3LDQuMDM4Njg5NDUgMzYuNDYwMDE1LDQuMjEzOTc3NTcgQzM2LjM3MjgyODIsNS4xNjk1ODcwNyAzNS41NTM5OTU2LDUuNTA4MzI0OTcgMzQuNjI2MDc5MSw1LjUwODMyNDk3IEMzMy43MDM4NTQ1LDUuNTA4MzI0OTcgMzIuODc5MzMsNS4xNjk1ODcwNyAzMi43OTM2MTY0LDQuMjEzOTc3NTcgQzMyLjc3NTkzOCw0LjAzODY4OTQ1IDMyLjc3Mjg1NzYsMy45NjQ0MzA3OCAzMi43NzI4NTc2LDMuODY4NTEzMzQgTDMyLjc3Mjg1NzYsMS42NjYxNzg4NCBDMzIuNzcyODU3NiwxLjU3MDI2MTQgMzIuNzg4NzI4LDEuNDA5MDk4NTcgMzIuNzk5MTA3NCwxLjMxOTUwMzg3IEMzMi45MTQxNTExLDAuMzM5NzQ2ODU1IDMzLjcwMzg1NDUsMC4wMjU5NjM2MjgyIDM0LjYyOTc2MjEsMC4wMjU5NjM2MjgyIFogTTEyLjE0NDc0NDcsMC4xNjY4ODAyMTEgTDEyLjgwMjI2MTYsNC4yNjE1OTk5OCBMMTMuNDYwMTgwNCwwLjE2Njg4MDIxMSBMMTUuNTg1Mjc0NiwwLjE2Njg4MDIxMSBMMTUuNzAxOTI1NSw1LjQwNTIxMDM2IEwxNC4zOTUyNjIsNS40MDUyMTAzNiBMMTQuMzU5ODM4MiwwLjU1NTkzMTA1NCBMMTMuNDYyMjU2Miw1LjQwNTIxMDM2IEwxMi4xMzk4NTYzLDUuNDA1MjEwMzYgTDExLjI0MzA3NzksMC41NTU5MzEwNTQgTDExLjIwNzc4OCw1LjQwNTIxMDM2IEw5LjkwNDQwNTgsNS40MDUyMTAzNiBMMTAuMDE3MjM5NywwLjE2Njg4MDIxMSBMMTIuMTQ0NzQ0NywwLjE2Njg4MDIxMSBaIE03LjkwMTI1MjUsMC4xNjY4ODAyMTEgTDguODYzMjUzNTgsNS40MDUyMTAzNiBMNy40NjQzMTQxLDUuNDA1MjEwMzYgTDYuNzUzODI4ODMsMC41NTU5MzEwNTQgTDYuMDI1ODY2MDIsNS40MDUyMTAzNiBMNC42MTcxNDk4Myw1LjQwNTIxMDM2IEw1LjU4MzE2ODczLDAuMTY2ODgwMjExIEw3LjkwMTI1MjUsMC4xNjY4ODAyMTEgWiBNMjguMzA0ODM2LDUuMzUwNTkyNTcgTDI3LjAyNDYyMzMsNS4zNTA1OTI1NyBMMjcuMDI0NjIzMywwLjE2Njg4MDIxMSBMMjguOTU5ODc1MywwLjE2Njg4MDIxMSBMMzAuMTg3OTkwMyw0LjM4NDM1NTQ3IEwzMC4xMTY4NzQ4LDAuMTY2ODgwMjExIEwzMS40MDU0NTgxLDAuMTY2ODgwMjExIEwzMS40MDU0NTgxLDUuMzUwNTkyNTcgTDI5LjU0OTQyNDEsNS4zNTA1OTI1NyBMMjguMjMsMC45OTkgTDI4LjMwNDgzNiw1LjM1MDU5MjU3IFoiPjwvcGF0aD48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iaTUiPjxwYXRoIGQ9Ik0yNC4zMzUyOTY2LDIuNDcwMDY0MzIgQzI1LjMwOTY1MDIsMi40NzAwNjQzMiAyNi4xMjEzMjMsMi42NTY2MDU2NCAyNi43NjkyNzY4LDMuMDI4OTczNTYgQzI3LjQxNzIzMDUsMy40MDE2OTg4MyAyNy45Mjk1MDE3LDMuOTA4NDMzNjcgMjguMzA2MDkwMiw0LjU1MDI1MDE1IEwyNi4zOTU0NTczLDUuNDgyNTk5MzcgQzI2LjE5NjA4NjksNS4xNDYzMjQ3IDI1LjkxOTE4MzYsNC44ODAwOTIzNiAyNS41NjQ3NDczLDQuNjg0NjE3MDggQzI1LjIxMDMxMTEsNC40ODkxNDE3OSAyNC44MDA0OTQyLDQuMzkxMjI1NDcgMjQuMzM1Mjk2Niw0LjM5MTIyNTQ3IEMyMy44MDM2NDIyLDQuMzkxMjI1NDcgMjMuNDEwNDM5NSw0LjQ5NDg1OTUzIDIzLjE1NTY4ODUsNC43MDIxMjc2NiBDMjIuOTAwNTkxMyw0LjkwOTM5NTc5IDIyLjc3MzU2MTksNS4xNDk1NDA5MyAyMi43NzM1NjE5LDUuNDIyMjA1NzMgQzIyLjc3MzU2MTksNS43Mzg0NjgzMSAyMi45NjE4NTYyLDUuOTY1MDMzODEgMjMuMzM4NDQ0Nyw2LjEwMDgzMDE3IEMyMy43MTQ2ODcsNi4yMzczNDEyNSAyNC4yNjg4Mzk4LDYuMzgyMDcxNTggMjQuOTk5ODY0Niw2LjUzNDY2MzgxIEMyNS4zOTg2MDU0LDYuNjExMTM4NiAyNS43OTk3NjksNi43MTE1NTY0NCAyNi4yMDQzOTQsNi44MzczNDY3NSBDMjYuNjA4NjcyOSw2Ljk2Mjc3OTcgMjYuOTc2OTU0Myw3LjEzMTgxMDQzIDI3LjMwOTIzODMsNy4zNDQ3OTYzMSBDMjcuNjQxNTIyMiw3LjU1NzQyNDgyIDI3LjkxMDExODUsNy44Mjk3MzIyNiAyOC4xMTUwMjY5LDguMTYyNzkwNyBDMjguMzE5OTM1NCw4LjQ5NTQ5MTc4IDI4LjQyMjM4OTYsOC45MTI1Mjk1NSAyOC40MjIzODk2LDkuNDE0MjYxMzcgQzI4LjQyMjM4OTYsOS43NTIzMjI4MyAyOC4zNDQ4NTY3LDEwLjEwNDMyMTMgMjguMTg5NzkwOCwxMC40Njk1NDIgQzI4LjAzNDM3ODgsMTAuODM0NzYyOCAyNy43OTM4MTkxLDExLjE3MzE4MTYgMjcuNDY3MDczMSwxMS40ODM3MjY0IEMyNy4xNDAzMjcyLDExLjc5NDk4NiAyNi43Mjc3NDEzLDEyLjA0ODM1MzQgMjYuMjI5MzE1MywxMi4yNDQ1NDM0IEMyNS43MzA4ODkzLDEyLjQ0MTA5MDggMjUuMTMyNzc4MiwxMi41MzkwMDcxIDI0LjQzNDk4MTgsMTIuNTM5MDA3MSBDMjMuMzYwNTk2OSwxMi41MzkwMDcxIDIyLjQ2ODk2ODIsMTIuMzMyODExIDIxLjc2MDA5NTcsMTEuOTIwMDYxNiBDMjEuMDUxMjIzMywxMS41MDY5NTQ4IDIwLjUwMjk1NDcsMTAuOTEwNTIyOCAyMC4xMTUyOSwxMC4xMzAwNTExIEwyMi4xOTIwNjQ5LDkuMTY1MTgyMjUgQzIyLjQyNDY2MzcsOS42MDQ3MzM2MyAyMi43NDAzMzM1LDkuOTQyNDM3NzQgMjMuMTM5MDc0MywxMC4xNzg2NTE5IEMyMy41Mzc4MTUxLDEwLjQxNDUwODggMjQuMDAzMDEyNiwxMC41MzIwNzk4IDI0LjUzNDY2NywxMC41MzIwNzk4IEMyNS4wODg0NzM2LDEwLjUzMjA3OTggMjUuNTAzODI4NiwxMC40MTc3MjUgMjUuNzgwNzMxOSwxMC4xODkwMTUzIEMyNi4wNTcyODkxLDkuOTU5OTQ4MzIgMjYuMTk2MDg2OSw5LjY4NzY0MDg4IDI2LjE5NjA4NjksOS4zNzEwMjA5NSBDMjYuMTk2MDg2OSw5LjE5NjYyOTgzIDI2LjEzMjM5OTIsOS4wNTUxMTU3MyAyNi4wMDUwMjM2LDguOTQ1NzYzOTIgQzI1Ljg3NzY0ODEsOC44MzcxMjY4MyAyNS43MTE1MDYxLDguNzQxMzU0NjYgMjUuNTA2NTk3Niw4LjY1OTg3Njg1IEMyNS4zMDEzNDMxLDguNTc4MDQxNjcgMjUuMDYwNzgzMyw4LjUxMDE0MzQ5IDI0Ljc4Mzg4LDguNDU1MTEwMjMgQzI0LjUwNjk3NjcsOC40MDA3OTE2OSAyNC4yMTg5OTcyLDguMzQwNzU1NCAyMy45MTk5NDE2LDguMjc1MzU4NzMgQzIzLjQ5ODcwMjUsOC4xODgxNjMxOCAyMy4wODY0NjI2LDguMDg0ODg2NDcgMjIuNjgyMTgzOCw3Ljk2NDgxMzkgQzIyLjI3NzkwNSw3Ljg0NTA5ODY5IDIxLjkxNTE2MTYsNy42Nzg1Njk0NyAyMS41OTM5NTM4LDcuNDY2Mjk4MzEgQzIxLjI3Mjc0NTksNy4yNTMzMTI0NCAyMS4wMTI0NTY4LDYuOTgwNjQ3NjQgMjAuODEzMDg2NCw2LjY0ODMwMzkyIEMyMC42MTM3MTYsNi4zMTU5NjAyIDIwLjUxNDAzMDgsNS44OTg5MjI0MyAyMC41MTQwMzA4LDUuMzk3NTQ3OTcgQzIwLjUxNDAzMDgsNS4wMTU1MzEzNyAyMC42MDU0MDg5LDQuNjQ3ODA5MTIgMjAuNzg4MTY1MSw0LjI5MzY2NjUgQzIwLjk3MDkyMTMsMy45MzkxNjY1MyAyMS4yMjg0NDE0LDMuNjI2MTIwMTggMjEuNTYwNzI1NCwzLjM1MzA5ODAzIEMyMS44OTMwMDkzLDMuMDgwNzkwNTkgMjIuMjk0MTczLDIuODY1NjYwNTYgMjIuNzY1MjU0OCwyLjcwNzM1MDYgQzIzLjIzNTk5MDQsMi41NDkwNDA2MyAyMy43NTkzMzc3LDIuNDcwMDY0MzIgMjQuMzM1Mjk2NiwyLjQ3MDA2NDMyIFogTTMzLjEwNzM1MTUsMi40NzAwNjQzMiBDMzQuMDgxNzA1LDIuNDcwMDY0MzIgMzQuODkzMzc3OSwyLjY1NjYwNTY0IDM1LjU0MTMzMTYsMy4wMjg5NzM1NiBDMzYuMTg5Mjg1NCwzLjQwMTY5ODgzIDM2LjcwMTU1NjUsMy45MDg0MzM2NyAzNy4wNzgxNDUxLDQuNTUwMjUwMTUgTDM1LjE2NzUxMjIsNS40ODI1OTkzNyBDMzQuOTY4MTQxOCw1LjE0NjMyNDcgMzQuNjkxMjM4NCw0Ljg4MDA5MjM2IDM0LjMzNjgwMjIsNC42ODQ2MTcwOCBDMzMuOTgyMzY1OSw0LjQ4OTE0MTc5IDMzLjU3MjU0OSw0LjM5MTIyNTQ3IDMzLjEwNzM1MTUsNC4zOTEyMjU0NyBDMzIuNTc1Njk3MSw0LjM5MTIyNTQ3IDMyLjE4MjQ5NDQsNC40OTQ4NTk1MyAzMS45Mjc3NDMzLDQuNzAyMTI3NjYgQzMxLjY3MjY0NjEsNC45MDkzOTU3OSAzMS41NDU2MTY3LDUuMTQ5NTQwOTMgMzEuNTQ1NjE2Nyw1LjQyMjIwNTczIEMzMS41NDU2MTY3LDUuNzM4NDY4MzEgMzEuNzMzOTExLDUuOTY1MDMzODEgMzIuMTEwNDk5NSw2LjEwMDgzMDE3IEMzMi40ODY3NDE5LDYuMjM3MzQxMjUgMzMuMDQwODk0Nyw2LjM4MjA3MTU4IDMzLjc3MTkxOTQsNi41MzQ2NjM4MSBDMzQuMTcwNjYwMiw2LjYxMTEzODYgMzQuNTcxODIzOSw2LjcxMTU1NjQ0IDM0Ljk3NjQ0ODksNi44MzczNDY3NSBDMzUuMzgwNzI3Nyw2Ljk2Mjc3OTcgMzUuNzQ5MDA5MSw3LjEzMTgxMDQzIDM2LjA4MTI5MzEsNy4zNDQ3OTYzMSBDMzYuNDEzNTc3MSw3LjU1NzQyNDgyIDM2LjY4MjE3MzMsNy44Mjk3MzIyNiAzNi44ODcwODE4LDguMTYyNzkwNyBDMzcuMDkxOTkwMiw4LjQ5NTQ5MTc4IDM3LjE5NDQ0NDQsOC45MTI1Mjk1NSAzNy4xOTQ0NDQ0LDkuNDE0MjYxMzcgQzM3LjE5NDQ0NDQsOS43NTIzMjI4MyAzNy4xMTY5MTE1LDEwLjEwNDMyMTMgMzYuOTYxODQ1NywxMC40Njk1NDIgQzM2LjgwNjQzMzcsMTAuODM0NzYyOCAzNi41NjU4NzM5LDExLjE3MzE4MTYgMzYuMjM5MTI4LDExLjQ4MzcyNjQgQzM1LjkxMjM4MjEsMTEuNzk0OTg2IDM1LjQ5OTc5NjEsMTIuMDQ4MzUzNCAzNS4wMDEzNzAyLDEyLjI0NDU0MzQgQzM0LjUwMjk0NDIsMTIuNDQxMDkwOCAzMy45MDQ4MzMsMTIuNTM5MDA3MSAzMy4yMDcwMzY3LDEyLjUzOTAwNzEgQzMyLjEzMjY1MTgsMTIuNTM5MDA3MSAzMS4yNDEwMjMxLDEyLjMzMjgxMSAzMC41MzIxNTA2LDExLjkyMDA2MTYgQzI5LjgyMzI3ODEsMTEuNTA2OTU0OCAyOS4yNzUwMDk1LDEwLjkxMDUyMjggMjguODg3MzQ0OSwxMC4xMzAwNTExIEwzMC45NjQxMTk4LDkuMTY1MTgyMjUgQzMxLjE5NjcxODYsOS42MDQ3MzM2MyAzMS41MTIzODgzLDkuOTQyNDM3NzQgMzEuOTExMTI5MSwxMC4xNzg2NTE5IEMzMi4zMDk4Njk5LDEwLjQxNDUwODggMzIuNzc1MDY3NSwxMC41MzIwNzk4IDMzLjMwNjcyMTgsMTAuNTMyMDc5OCBDMzMuODYwNTI4NSwxMC41MzIwNzk4IDM0LjI3NTg4MzUsMTAuNDE3NzI1IDM0LjU1Mjc4NjgsMTAuMTg5MDE1MyBDMzQuODI5MzQ0LDkuOTU5OTQ4MzIgMzQuOTY4MTQxOCw5LjY4NzY0MDg4IDM0Ljk2ODE0MTgsOS4zNzEwMjA5NSBDMzQuOTY4MTQxOCw5LjE5NjYyOTgzIDM0LjkwNDQ1NCw5LjA1NTExNTczIDM0Ljc3NzA3ODUsOC45NDU3NjM5MiBDMzQuNjQ5NzAyOSw4LjgzNzEyNjgzIDM0LjQ4MzU2MSw4Ljc0MTM1NDY2IDM0LjI3ODY1MjUsOC42NTk4NzY4NSBDMzQuMDczMzk3OSw4LjU3ODA0MTY3IDMzLjgzMjgzODIsOC41MTAxNDM0OSAzMy41NTU5MzQ4LDguNDU1MTEwMjMgQzMzLjI3OTAzMTUsOC40MDA3OTE2OSAzMi45OTEwNTIxLDguMzQwNzU1NCAzMi42OTE5OTY1LDguMjc1MzU4NzMgQzMyLjI3MDc1NzMsOC4xODgxNjMxOCAzMS44NTg1MTc1LDguMDg0ODg2NDcgMzEuNDU0MjM4Niw3Ljk2NDgxMzkgQzMxLjA0OTk1OTgsNy44NDUwOTg2OSAzMC42ODcyMTY1LDcuNjc4NTY5NDcgMzAuMzY2MDA4Niw3LjQ2NjI5ODMxIEMzMC4wNDQ4MDA4LDcuMjUzMzEyNDQgMjkuNzg0NTExNiw2Ljk4MDY0NzY0IDI5LjU4NTE0MTIsNi42NDgzMDM5MiBDMjkuMzg1NzcwOSw2LjMxNTk2MDIgMjkuMjg2MDg1Nyw1Ljg5ODkyMjQzIDI5LjI4NjA4NTcsNS4zOTc1NDc5NyBDMjkuMjg2MDg1Nyw1LjAxNTUzMTM3IDI5LjM3NzQ2MzgsNC42NDc4MDkxMiAyOS41NjAyMTk5LDQuMjkzNjY2NSBDMjkuNzQyOTc2MSwzLjkzOTE2NjUzIDMwLjAwMDQ5NjIsMy42MjYxMjAxOCAzMC4zMzI3ODAyLDMuMzUzMDk4MDMgQzMwLjY2NTA2NDIsMy4wODA3OTA1OSAzMS4wNjYyMjc5LDIuODY1NjYwNTYgMzEuNTM3MzA5NiwyLjcwNzM1MDYgQzMyLjAwODA0NTMsMi41NDkwNDA2MyAzMi41MzEzOTI2LDIuNDcwMDY0MzIgMzMuMTA3MzUxNSwyLjQ3MDA2NDMyIFogTTEzLjc3MzIwNTcsMi40NzAwMjg1OSBDMTQuNDE1NjIxNCwyLjQ3MDAyODU5IDE1LjAwODE5NDUsMi41OTAxMDExNiAxNS41NTA5MjUsMi44Mjk1MzE1OSBDMTYuMDkzMzA5NCwzLjA2OTY3NjczIDE2LjU0MjIzODksMy4zOTY2NjAwNyAxNi44OTY2NzUxLDMuODEwODM4OTcgTDE2Ljg5NjY3NTEsMi40ODcxODE4MSBMMTkuMTM5NTkyLDIuNDg3MTgxODEgTDE5LjEzOTU5MiwxMi41MjE4MTgxIEwxNi44OTY2NzUxLDEyLjUyMTgxODEgTDE2Ljg5NjY3NTEsMTEuMDk1OTU2MyBDMTYuNTQyMjM4OSwxMS41NDQwODQzIDE2LjA4ODExNzQsMTEuODk2Nzk3NSAxNS41MzQzMTA4LDEyLjE1MzczODUgQzE0Ljk4MDUwNDIsMTIuNDEwNjc5NSAxNC4zODIzOTMsMTIuNTM4OTcxNCAxMy43Mzk5NzczLDEyLjUzODk3MTQgQzEzLjE1Mjk0MjMsMTIuNTM4OTcxNCAxMi41NzQyMTQzLDEyLjQyNjQwMzMgMTIuMDAzNzkzNSwxMi4yMDA1NTI1IEMxMS40MzMzNzI2LDExLjk3NDM0NDQgMTAuOTIxMTAxNSwxMS42NDYyODkgMTAuNDY2OTgwMSwxMS4yMTYzODYzIEMxMC4wMTI4NTg2LDEwLjc4NjQ4MzYgOS42NDcwMDAxMSwxMC4yNjAwOTQgOS4zNzA0NDI5Miw5LjYzNzU3NDkxIEM5LjA5MzUzOTYsOS4wMTQ2OTg0NCA4Ljk1NTA4Nzk0LDguMzA2NDEzMjIgOC45NTUwODc5NCw3LjUxMzA3NjU4IEM4Ljk1NTA4Nzk0LDYuNzA4MzA0NDcgOS4wOTA0MjQ0NCw1Ljk5NTAxNjIyIDkuMzYyMTM1ODIsNS4zNzE3ODI0IEM5LjYzMzUwMTA3LDQuNzQ4OTA1OTMgOS45OTM0NzUzOSw0LjIyMjg3MzcxIDEwLjQ0MjA1ODgsMy43OTI5NzEwMyBDMTAuODkwNjQyMSwzLjM2MjcxMDk4IDExLjQwNTY4MjMsMy4wMzUwMTI5MiAxMS45ODcxNzkzLDIuODA5NTE5NDkgQzEyLjU2ODY3NjMsMi41ODMzMTEzNCAxMy4xNjQwMTg0LDIuNDcwMDI4NTkgMTMuNzczMjA1NywyLjQ3MDAyODU5IFogTTQuMTUzNTQ5NzgsMCBDNC43ODQ4ODkzNSwwIDUuMzY2Mzg2MzIsMC4xMTcyMTM3MDEgNS44OTgwNDA2OSwwLjM1MTk5ODQ2MSBDNi40Mjk2OTUwNiwwLjU4NjQyNTg2MiA2Ljg4OTAwODQ0LDAuOTAzNDAzMTU2IDcuMjc3MDE5MjIsMS4zMDQwMDI0MiBDNy42NjQ2ODM4NiwxLjcwNDI0NDMyIDcuOTY4OTMxMzgsMi4xNzU5NTggOC4xOTA4MDAxNywyLjcxOTE0MzQ0IEM4LjQxMjMyMjgyLDMuMjYxOTcxNTIgOC41MjMwODQxNSwzLjg0MjMyMjI4IDguNTIzMDg0MTUsNC40NjAxOTU3MiBDOC41MjMwODQxNSw1LjA3NzM1NDQ0IDguNDEyMzIyODIsNS42NjA1NjQwOCA4LjE5MDgwMDE3LDYuMjA5NDY3MjYgQzcuOTY4OTMxMzgsNi43NTgzNzA0NCA3LjY2NDY4Mzg2LDcuMjMyOTQyOTkgNy4yNzcwMTkyMiw3LjYzMzU0MjI1IEM2Ljg4OTAwODQ0LDguMDMzNzg0MTYgNi40MjY5MjYwMyw4LjM1MTExODgxIDUuODg5NzMzNTksOC41ODUxODg4NSBDNS4zNTIxOTUwMiw4LjgxOTYxNjI1IDQuNzY4Mjc1MTUsOC45MzY4Mjk5NSA0LjEzNjkzNTU4LDguOTM2ODI5OTUgTDIuMjU5NTMxMDgsOC45MzY4Mjk5NSBMMi4yNTk1MzEwOCwxMi41MjE4NTM5IEwwLDEyLjUyMTg1MzkgTDAsMCBMNC4xNTM1NDk3OCwwIFogTTE0LjEwNTQ4OTcsNC41ODAyMzI1NiBDMTMuNjk1NjcyOCw0LjU4MDIzMjU2IDEzLjMxMDc3NzEsNC42NTU2MzUyNyAxMi45NTA4MDI4LDQuODA1NzI1OTkgQzEyLjU5MDgyODUsNC45NTU4MTY3IDEyLjI3NzkyNzgsNS4xNjAyMjU5NiAxMi4wMTIxMDA2LDUuNDE3NTI0MzMgQzExLjc0NjI3MzQsNS42NzU1Mzc0MSAxMS41Mzg1OTU5LDUuOTgxNzkzOTQgMTEuMzg5MDY4MSw2LjMzNjI5MzkxIEMxMS4yMzk1NDAzLDYuNjkwNzkzODkgMTEuMTY0Nzc2NCw3LjA3MTczODQxIDExLjE2NDc3NjQsNy40ODAxOTk1NyBDMTEuMTY0Nzc2NCw3Ljg4ODMwMzM3IDExLjIzOTU0MDMsOC4yNzI0NjQxMyAxMS4zODkwNjgxLDguNjMxOTY3MTIgQzExLjUzODU5NTksOC45OTE0NzAxMiAxMS43NDYyNzM0LDkuMzAyNzI5NjcgMTIuMDEyMTAwNiw5LjU2NjQ2MDUgQzEyLjI3NzkyNzgsOS44Mjk0NzY2MSAxMi41OTA4Mjg1LDEwLjAzNjAzIDEyLjk1MDgwMjgsMTAuMTg2ODM1NCBDMTMuMzEwNzc3MSwxMC4zMzY5MjYyIDEzLjY5NTY3MjgsMTAuNDExOTcxNSAxNC4xMDU0ODk3LDEwLjQxMTk3MTUgQzE0LjUyNjM4MjcsMTAuNDExOTcxNSAxNC45MTM3MDEyLDEwLjMzNjkyNjIgMTUuMjY4NDgzNiwxMC4xODY4MzU0IEMxNS42MjI5MTk5LDEwLjAzNjAzIDE1LjkyNzE2NzQsOS44MjY2MTc3NCAxNi4xODIyNjQ2LDkuNTU3ODgzODkgQzE2LjQzNzAxNTYsOS4yODk1MDczOSAxNi42MzkxNTUsOC45Nzc4OTA0OCAxNi43ODg2ODI4LDguNjIzNzQ3ODcgQzE2LjkzODIxMDYsOC4yNjk2MDUyNiAxNy4wMTI5NzQ1LDcuODg4MzAzMzcgMTcuMDEyOTc0NSw3LjQ4MDE5OTU3IEMxNy4wMTI5NzQ1LDcuMDgyODE2NTQgMTYuOTM4MjEwNiw2LjcwNjg3NTAzIDE2Ljc4ODY4MjgsNi4zNTIzNzUwNiBDMTYuNjM5MTU1LDUuOTk3ODc1MDkgMTYuNDM3MDE1Niw1LjY4OTExNzA1IDE2LjE4MjI2NDYsNS40MjYxMDA5NCBDMTUuOTI3MTY3NCw1LjE2MjcyNzQ3IDE1LjYyMjkxOTksNC45NTU4MTY3IDE1LjI2ODQ4MzYsNC44MDU3MjU5OSBDMTQuOTEzNzAxMiw0LjY1NTYzNTI3IDE0LjUyNjM4MjcsNC41ODAyMzI1NiAxNC4xMDU0ODk3LDQuNTgwMjMyNTYgWiBNMy45ODc0MDc3OSwyLjE2MTMwNjI4IEwyLjI1OTUzMTA4LDIuMTYxMzA2MjggTDIuMjU5NTMxMDgsNi43NzU1MjM2NyBMMy45ODc0MDc3OSw2Ljc3NTUyMzY3IEM0LjMxOTY5MTc3LDYuNzc1NTIzNjcgNC42MjQyODU0Miw2LjcxNTQ4NzM4IDQuOTAxMTg4NzQsNi41OTU0MTQ4MSBDNS4xNzc3NDU5Myw2LjQ3NTM0MjI0IDUuNDE2MjI4OTIsNi4zMDk1Mjc3NCA1LjYxNTU5OTMsNi4wOTgzMjg2NiBDNS44MTQ5Njk2OSw1Ljg4Njc3MjIyIDUuOTcwMDM1NTUsNS42NDA1NTE5OCA2LjA4MDc5Njg4LDUuMzYwMzgyNjUgQzYuMTkxMjEyMDgsNS4wODA1NzA2NyA2LjI0NjkzODg3LDQuNzgwMDMxODkgNi4yNDY5Mzg4Nyw0LjQ2MDE5NTcyIEM2LjI0NjkzODg3LDQuMTM5NjQ0ODQgNi4xOTEyMTIwOCwzLjgzOTQ2MzQxIDYuMDgwNzk2ODgsMy41NTkyOTQwOCBDNS45NzAwMzU1NSwzLjI3OTEyNDc1IDUuODE0OTY5NjksMy4wMzYxMjA3MyA1LjYxNTU5OTMsMi44MzAyODIwNCBDNS40MTYyMjg5MiwyLjYyNDQ0MzM0IDUuMTc3NzQ1OTMsMi40NjE0ODc3MSA0LjkwMTE4ODc0LDIuMzQxNDE1MTQgQzQuNjI0Mjg1NDIsMi4yMjEzNDI1NyA0LjMxOTY5MTc3LDIuMTYxMzA2MjggMy45ODc0MDc3OSwyLjE2MTMwNjI4IFoiPjwvcGF0aD48L2NsaXBQYXRoPjwvZGVmcz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjMzLjAgLTE2MC4wKSI+PGcgY2xpcC1wYXRoPSJ1cmwoI2kwKSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjMzLjAgMTYwLjApIj48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMC4wMDAxODA1NTU1NTU1NTIwNzU0OCAwLjApIj48ZyBjbGlwLXBhdGg9InVybCgjaTEpIj48cG9seWdvbiBwb2ludHM9IjAsMCA1MiwwIDUyLDUyIDAsNTIgMCwwIiBzdHJva2U9Im5vbmUiIGZpbGw9InVybCgjaTIpIj48L3BvbHlnb24+PC9nPjwvZz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3LjU4MzMzMzMzMzMzMzMzMiAxNC44MDU1NTU1NTU1NTU1NSkiPjxnIGNsaXAtcGF0aD0idXJsKCNpMykiPjxnIGNsaXAtcGF0aD0idXJsKCNpNCkiPjxwb2x5Z29uIHBvaW50cz0iMCwwIDM2LjQ3OTE2NjcsMCAzNi40NzkxNjY3LDUuNjEyMTc5NDkgMCw1LjYxMjE3OTQ5IDAsMCIgc3Ryb2tlPSJub25lIiBmaWxsPSIjRkZGRkZGIj48L3BvbHlnb24+PC9nPjwvZz48L2c+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNy41ODMzMzMzMzMzMzMzNzEgMjQuNDMyMDgwMjU1NDc3MzMpIj48ZyBjbGlwLXBhdGg9InVybCgjaTUpIj48cG9seWdvbiBwb2ludHM9IjAsMCAzNy4xOTQ0NDQ0LDAgMzcuMTk0NDQ0NCwxMi41MzkwMDcxIDAsMTIuNTM5MDA3MSAwLDAiIHN0cm9rZT0ibm9uZSIgZmlsbD0iI0ZGRkZGRiI+PC9wb2x5Z29uPjwvZz48L2c+PC9nPjwvZz48L2c+PC9zdmc+" + }, + "66a0ccb3-bd6a-191f-ee06-e375c50b9846": { + "name": "Thales Bio iOS SDK", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3Ny42IDc3LjYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDc3LjYgNzcuNjsiPjxnPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik03Ny42LDU1LjFjLTQuOSwxLjQtMTEuNCwxLjktMTYuMiwybC0yMi43LTQ1LjhoLTEuM2wtMjIuNiw0NS44Yy00LjgtMC4xLTEwLjUtMC42LTE1LjQtMmwyOC43LTUzLjdoMjAuNSBMNzcuNiw1NS4xeiI+PC9wYXRoPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik00Ny43LDQxLjRjMCw1LjMtNC4zLDkuNS05LjYsOS41Yy01LjMsMC05LjUtNC4zLTkuNS05LjVjMC01LjMsNC4zLTkuNSw5LjUtOS41IEM0My40LDMxLjksNDcuNywzNi4xLDQ3LjcsNDEuNCI+PC9wYXRoPjwvZz48L3N2Zz4=", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3Ny42IDc3LjYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDc3LjYgNzcuNjsiPjxnPjxwYXRoIGZpbGw9IiMyQzJGNzMiIGQ9Ik03Ny42LDU1LjFjLTQuOSwxLjQtMTEuNCwxLjktMTYuMiwybC0yMi43LTQ1LjhoLTEuM2wtMjIuNiw0NS44Yy00LjgtMC4xLTEwLjUtMC42LTE1LjQtMmwyOC43LTUzLjdoMjAuNSBMNzcuNiw1NS4xeiI+PC9wYXRoPjxwYXRoIGZpbGw9IiM1RUJGRDQiIGQ9Ik00Ny43LDQxLjRjMCw1LjMtNC4zLDkuNS05LjYsOS41Yy01LjMsMC05LjUtNC4zLTkuNS05LjVjMC01LjMsNC4zLTkuNSw5LjUtOS41IEM0My40LDMxLjksNDcuNywzNi4xLDQ3LjcsNDEuNCI+PC9wYXRoPjwvZz48L3N2Zz4=" + }, + "8836336a-f590-0921-301d-46427531eee6": { + "name": "Thales Bio Android SDK", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3Ny42IDc3LjYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDc3LjYgNzcuNjsiPjxnPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik03Ny42LDU1LjFjLTQuOSwxLjQtMTEuNCwxLjktMTYuMiwybC0yMi43LTQ1LjhoLTEuM2wtMjIuNiw0NS44Yy00LjgtMC4xLTEwLjUtMC42LTE1LjQtMmwyOC43LTUzLjdoMjAuNSBMNzcuNiw1NS4xeiI+PC9wYXRoPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik00Ny43LDQxLjRjMCw1LjMtNC4zLDkuNS05LjYsOS41Yy01LjMsMC05LjUtNC4zLTkuNS05LjVjMC01LjMsNC4zLTkuNSw5LjUtOS41IEM0My40LDMxLjksNDcuNywzNi4xLDQ3LjcsNDEuNCI+PC9wYXRoPjwvZz48L3N2Zz4=", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3Ny42IDc3LjYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDc3LjYgNzcuNjsiPjxnPjxwYXRoIGZpbGw9IiMyQzJGNzMiIGQ9Ik03Ny42LDU1LjFjLTQuOSwxLjQtMTEuNCwxLjktMTYuMiwybC0yMi43LTQ1LjhoLTEuM2wtMjIuNiw0NS44Yy00LjgtMC4xLTEwLjUtMC42LTE1LjQtMmwyOC43LTUzLjdoMjAuNSBMNzcuNiw1NS4xeiI+PC9wYXRoPjxwYXRoIGZpbGw9IiM1RUJGRDQiIGQ9Ik00Ny43LDQxLjRjMCw1LjMtNC4zLDkuNS05LjYsOS41Yy01LjMsMC05LjUtNC4zLTkuNS05LjVjMC01LjMsNC4zLTkuNSw5LjUtOS41IEM0My40LDMxLjksNDcuNywzNi4xLDQ3LjcsNDEuNCI+PC9wYXRoPjwvZz48L3N2Zz4=" + }, + "cd69adb5-3c7a-deb9-3177-6800ea6cb72a": { + "name": "Thales PIN Android SDK", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3Ny42IDc3LjYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDc3LjYgNzcuNjsiPjxnPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik03Ny42LDU1LjFjLTQuOSwxLjQtMTEuNCwxLjktMTYuMiwybC0yMi43LTQ1LjhoLTEuM2wtMjIuNiw0NS44Yy00LjgtMC4xLTEwLjUtMC42LTE1LjQtMmwyOC43LTUzLjdoMjAuNSBMNzcuNiw1NS4xeiI+PC9wYXRoPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik00Ny43LDQxLjRjMCw1LjMtNC4zLDkuNS05LjYsOS41Yy01LjMsMC05LjUtNC4zLTkuNS05LjVjMC01LjMsNC4zLTkuNSw5LjUtOS41IEM0My40LDMxLjksNDcuNywzNi4xLDQ3LjcsNDEuNCI+PC9wYXRoPjwvZz48L3N2Zz4=", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3Ny42IDc3LjYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDc3LjYgNzcuNjsiPjxnPjxwYXRoIGZpbGw9IiMyQzJGNzMiIGQ9Ik03Ny42LDU1LjFjLTQuOSwxLjQtMTEuNCwxLjktMTYuMiwybC0yMi43LTQ1LjhoLTEuM2wtMjIuNiw0NS44Yy00LjgtMC4xLTEwLjUtMC42LTE1LjQtMmwyOC43LTUzLjdoMjAuNSBMNzcuNiw1NS4xeiI+PC9wYXRoPjxwYXRoIGZpbGw9IiM1RUJGRDQiIGQ9Ik00Ny43LDQxLjRjMCw1LjMtNC4zLDkuNS05LjYsOS41Yy01LjMsMC05LjUtNC4zLTkuNS05LjVjMC01LjMsNC4zLTkuNSw5LjUtOS41IEM0My40LDMxLjksNDcuNywzNi4xLDQ3LjcsNDEuNCI+PC9wYXRoPjwvZz48L3N2Zz4=" + }, + "17290f1e-c212-34d0-1423-365d729f09d9": { + "name": "Thales PIN iOS SDK", + "icon_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3Ny42IDc3LjYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDc3LjYgNzcuNjsiPjxnPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik03Ny42LDU1LjFjLTQuOSwxLjQtMTEuNCwxLjktMTYuMiwybC0yMi43LTQ1LjhoLTEuM2wtMjIuNiw0NS44Yy00LjgtMC4xLTEwLjUtMC42LTE1LjQtMmwyOC43LTUzLjdoMjAuNSBMNzcuNiw1NS4xeiI+PC9wYXRoPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik00Ny43LDQxLjRjMCw1LjMtNC4zLDkuNS05LjYsOS41Yy01LjMsMC05LjUtNC4zLTkuNS05LjVjMC01LjMsNC4zLTkuNSw5LjUtOS41IEM0My40LDMxLjksNDcuNywzNi4xLDQ3LjcsNDEuNCI+PC9wYXRoPjwvZz48L3N2Zz4=", + "icon_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3Ny42IDc3LjYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDc3LjYgNzcuNjsiPjxnPjxwYXRoIGZpbGw9IiMyQzJGNzMiIGQ9Ik03Ny42LDU1LjFjLTQuOSwxLjQtMTEuNCwxLjktMTYuMiwybC0yMi43LTQ1LjhoLTEuM2wtMjIuNiw0NS44Yy00LjgtMC4xLTEwLjUtMC42LTE1LjQtMmwyOC43LTUzLjdoMjAuNSBMNzcuNiw1NS4xeiI+PC9wYXRoPjxwYXRoIGZpbGw9IiM1RUJGRDQiIGQ9Ik00Ny43LDQxLjRjMCw1LjMtNC4zLDkuNS05LjYsOS41Yy01LjMsMC05LjUtNC4zLTkuNS05LjVjMC01LjMsNC4zLTkuNSw5LjUtOS41IEM0My40LDMxLjksNDcuNywzNi4xLDQ3LjcsNDEuNCI+PC9wYXRoPjwvZz48L3N2Zz4=" + } +} \ No newline at end of file diff --git a/selfservice/strategy/webauthn/errors.go b/x/webauthnx/errors.go similarity index 95% rename from selfservice/strategy/webauthn/errors.go rename to x/webauthnx/errors.go index 882a436ed179..f6347c35ddbc 100644 --- a/selfservice/strategy/webauthn/errors.go +++ b/x/webauthnx/errors.go @@ -1,7 +1,7 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -package webauthn +package webauthnx import ( "github.com/pkg/errors" @@ -9,6 +9,7 @@ import ( "github.com/ory/jsonschema/v3" ) +var ErrNoCredentials = errors.New("required credentials not found") + var ErrNotEnoughCredentials = &jsonschema.ValidationError{ Message: "unable to remove this security key because it would lock you out of your account", InstancePtr: "#/webauthn_remove"} -var ErrNoCredentials = errors.New("required credentials not found") diff --git a/selfservice/strategy/webauthn/handler.go b/x/webauthnx/handler.go similarity index 74% rename from selfservice/strategy/webauthn/handler.go rename to x/webauthnx/handler.go index 7036b5bdf01b..1d71419ee193 100644 --- a/selfservice/strategy/webauthn/handler.go +++ b/x/webauthnx/handler.go @@ -1,7 +1,7 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -package webauthn +package webauthnx import ( _ "embed" @@ -15,9 +15,12 @@ import ( //go:embed js/webauthn.js var jsOnLoad []byte -const webAuthnRoute = "/.well-known/ory/webauthn.js" +const ScriptURL = "/.well-known/ory/webauthn.js" // swagger:model webAuthnJavaScript +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions type webAuthnJavaScript string // swagger:route GET /.well-known/ory/webauthn.js frontend getWebAuthnJavaScript @@ -41,11 +44,11 @@ type webAuthnJavaScript string // // Responses: // 200: webAuthnJavaScript -func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) { - if handle, _, _ := r.Lookup("GET", webAuthnRoute); handle == nil { - r.GET(webAuthnRoute, func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { +func RegisterWebauthnRoute(r *x.RouterPublic) { + if handle, _, _ := r.Lookup("GET", ScriptURL); handle == nil { + r.GET(ScriptURL, func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { w.Header().Set("Content-Type", "text/javascript; charset=UTF-8") - _, _ = w.Write([]byte(webAuthnJavaScript(jsOnLoad))) + _, _ = w.Write(jsOnLoad) }) } } diff --git a/x/webauthnx/js/trigger.go b/x/webauthnx/js/trigger.go new file mode 100644 index 000000000000..7b236191ce8e --- /dev/null +++ b/x/webauthnx/js/trigger.go @@ -0,0 +1,22 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package js + +import "fmt" + +// swagger:enum WebAuthnTriggers +type WebAuthnTriggers string + +const ( + WebAuthnTriggersWebAuthnRegistration WebAuthnTriggers = "oryWebAuthnRegistration" + WebAuthnTriggersWebAuthnLogin WebAuthnTriggers = "oryWebAuthnLogin" + WebAuthnTriggersPasskeyLogin WebAuthnTriggers = "oryPasskeyLogin" + WebAuthnTriggersPasskeyLoginAutocompleteInit WebAuthnTriggers = "oryPasskeyLoginAutocompleteInit" + WebAuthnTriggersPasskeyRegistration WebAuthnTriggers = "oryPasskeyRegistration" + WebAuthnTriggersPasskeySettingsRegistration WebAuthnTriggers = "oryPasskeySettingsRegistration" +) + +func (r WebAuthnTriggers) String() string { + return fmt.Sprintf("window.%s", string(r)) +} diff --git a/x/webauthnx/js/trigger_test.go b/x/webauthnx/js/trigger_test.go new file mode 100644 index 000000000000..97f9dc00ee77 --- /dev/null +++ b/x/webauthnx/js/trigger_test.go @@ -0,0 +1,14 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package js + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestToString(t *testing.T) { + assert.Equal(t, "window.oryWebAuthnRegistration", WebAuthnTriggersWebAuthnRegistration.String()) +} diff --git a/x/webauthnx/js/webauthn.js b/x/webauthnx/js/webauthn.js new file mode 100644 index 000000000000..638bd4ece082 --- /dev/null +++ b/x/webauthnx/js/webauthn.js @@ -0,0 +1,405 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +;(function () { + if (!window) { + return + } + + if (!window.PublicKeyCredential) { + console.log("This browser does not support WebAuthn!") + return + } + + if (window.__oryWebAuthnInitialized) { + return + } + + function __oryWebAuthnBufferDecode(value) { + return Uint8Array.from( + atob(value.replaceAll("-", "+").replaceAll("_", "/")), + function (c) { + return c.charCodeAt(0) + }, + ) + } + + function __oryWebAuthnBufferEncode(value) { + return btoa(String.fromCharCode.apply(null, new Uint8Array(value))) + .replaceAll("+", "-") + .replaceAll("/", "_") + .replaceAll("=", "") + } + + function __oryWebAuthnLogin( + options, + resultQuerySelector = '*[name="webauthn_login"]', + triggerQuerySelector = '*[name="webauthn_login_trigger"]', + ) { + if (!window.PublicKeyCredential) { + alert("This browser does not support WebAuthn!") + } + + const triggerEl = document.querySelector(triggerQuerySelector) + let opt = options + if (!opt) { + opt = JSON.parse(triggerEl.value) + } + + opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) + opt.publicKey.allowCredentials = opt.publicKey.allowCredentials.map( + function (value) { + return { + ...value, + id: __oryWebAuthnBufferDecode(value.id), + } + }, + ) + + navigator.credentials + .get(opt) + .then(function (credential) { + document.querySelector(resultQuerySelector).value = JSON.stringify({ + id: credential.id, + rawId: __oryWebAuthnBufferEncode(credential.rawId), + type: credential.type, + response: { + authenticatorData: __oryWebAuthnBufferEncode( + credential.response.authenticatorData, + ), + clientDataJSON: __oryWebAuthnBufferEncode( + credential.response.clientDataJSON, + ), + signature: __oryWebAuthnBufferEncode(credential.response.signature), + userHandle: __oryWebAuthnBufferEncode( + credential.response.userHandle, + ), + }, + }) + + triggerEl.closest("form").submit() + }) + .catch((err) => { + alert(err) + }) + } + + function __oryWebAuthnRegistration( + options, + resultQuerySelector = '*[name="webauthn_register"]', + triggerQuerySelector = '*[name="webauthn_register_trigger"]', + ) { + if (!window.PublicKeyCredential) { + alert("This browser does not support WebAuthn!") + } + + const triggerEl = document.querySelector(triggerQuerySelector) + let opt = options + if (!opt) { + opt = JSON.parse(triggerEl.value) + } + + opt.publicKey.user.id = __oryWebAuthnBufferDecode(opt.publicKey.user.id) + opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) + + if (opt.publicKey.excludeCredentials) { + opt.publicKey.excludeCredentials = opt.publicKey.excludeCredentials.map( + function (value) { + return { + ...value, + id: __oryWebAuthnBufferDecode(value.id), + } + }, + ) + } + + navigator.credentials + .create(opt) + .then(function (credential) { + document.querySelector(resultQuerySelector).value = JSON.stringify({ + id: credential.id, + rawId: __oryWebAuthnBufferEncode(credential.rawId), + type: credential.type, + response: { + attestationObject: __oryWebAuthnBufferEncode( + credential.response.attestationObject, + ), + clientDataJSON: __oryWebAuthnBufferEncode( + credential.response.clientDataJSON, + ), + }, + }) + + triggerEl.closest("form").submit() + }) + .catch((err) => { + alert(err) + }) + } + + async function __oryPasskeyLoginAutocompleteInit () { + const dataEl = document.getElementsByName("passkey_challenge")[0] + const resultEl = document.getElementsByName("passkey_login")[0] + const identifierEl = document.getElementsByName("identifier")[0] + + if (!dataEl || !resultEl || !identifierEl) { + console.debug( + "__oryPasskeyLoginAutocompleteInit: mandatory fields not found", + ) + return + } + + if ( + !window.PublicKeyCredential || + !window.PublicKeyCredential.isConditionalMediationAvailable || + window.Cypress // Cypress auto-fills the autocomplete, which we don't want + ) { + console.log("This browser does not support WebAuthn!") + return + } + const isCMA = await PublicKeyCredential.isConditionalMediationAvailable() + if (!isCMA) { + console.log( + "This browser does not support WebAuthn Conditional Mediation!", + ) + return + } + + let opt = JSON.parse(dataEl.value) + + if (opt.publicKey.user && opt.publicKey.user.id) { + opt.publicKey.user.id = __oryWebAuthnBufferDecode(opt.publicKey.user.id) + } + opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) + + // Allow aborting through a global variable + window.abortPasskeyConditionalUI = new AbortController() + + navigator.credentials + .get({ + publicKey: opt.publicKey, + mediation: "conditional", + signal: abortPasskeyConditionalUI.signal, + }) + .then(function (credential) { + resultEl.value = JSON.stringify({ + id: credential.id, + rawId: __oryWebAuthnBufferEncode(credential.rawId), + type: credential.type, + response: { + authenticatorData: __oryWebAuthnBufferEncode( + credential.response.authenticatorData, + ), + clientDataJSON: __oryWebAuthnBufferEncode( + credential.response.clientDataJSON, + ), + signature: __oryWebAuthnBufferEncode(credential.response.signature), + userHandle: __oryWebAuthnBufferEncode( + credential.response.userHandle, + ), + }, + }) + + resultEl.closest("form").submit() + }) + .catch((err) => { + console.log(err) + }) + } + + function __oryPasskeyLogin () { + const dataEl = document.getElementsByName("passkey_challenge")[0] + const resultEl = document.getElementsByName("passkey_login")[0] + + if (!dataEl || !resultEl) { + console.debug("__oryPasskeyLogin: mandatory fields not found") + return + } + if (!window.PublicKeyCredential) { + console.log("This browser does not support WebAuthn!") + return + } + + let opt = JSON.parse(dataEl.value) + + if (opt.publicKey.user && opt.publicKey.user.id) { + opt.publicKey.user.id = __oryWebAuthnBufferDecode(opt.publicKey.user.id) + } + opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) + if (opt.publicKey.allowCredentials) { + opt.publicKey.allowCredentials = opt.publicKey.allowCredentials.map( + function (cred) { + return { + ...cred, + id: __oryWebAuthnBufferDecode(cred.id), + } + }, + ) + } + + window.abortPasskeyConditionalUI && + window.abortPasskeyConditionalUI.abort( + "only one credentials.get allowed at a time", + ) + + navigator.credentials + .get({ + publicKey: opt.publicKey, + }) + .then(function (credential) { + resultEl.value = JSON.stringify({ + id: credential.id, + rawId: __oryWebAuthnBufferEncode(credential.rawId), + type: credential.type, + response: { + authenticatorData: __oryWebAuthnBufferEncode( + credential.response.authenticatorData, + ), + clientDataJSON: __oryWebAuthnBufferEncode( + credential.response.clientDataJSON, + ), + signature: __oryWebAuthnBufferEncode(credential.response.signature), + userHandle: __oryWebAuthnBufferEncode( + credential.response.userHandle, + ), + }, + }) + + resultEl.closest("form").submit() + }) + .catch((err) => { + // Calling this again will enable the autocomplete once again. + console.error(err) + window.abortPasskeyConditionalUI && __oryPasskeyLoginAutocompleteInit() + }) + } + + function __oryPasskeyRegistration () { + const dataEl = document.getElementsByName("passkey_create_data")[0] + const resultEl = document.getElementsByName("passkey_register")[0] + + if (!dataEl || !resultEl) { + console.debug("__oryPasskeyRegistration: mandatory fields not found") + return + } + + const createData = JSON.parse(dataEl.value) + + // Fetch display name from field value + const displayNameFieldName = createData.displayNameFieldName + const displayName = dataEl + .closest("form") + .querySelector("[name='" + displayNameFieldName + "']").value + + let opts = createData.credentialOptions + opts.publicKey.user.name = displayName + opts.publicKey.user.displayName = displayName + opts.publicKey.user.id = __oryWebAuthnBufferDecode(opts.publicKey.user.id) + opts.publicKey.challenge = __oryWebAuthnBufferDecode( + opts.publicKey.challenge, + ) + + if (opts.publicKey.excludeCredentials) { + opts.publicKey.excludeCredentials = opts.publicKey.excludeCredentials.map( + function (value) { + return { + ...value, + id: __oryWebAuthnBufferDecode(value.id), + } + }, + ) + } + + navigator.credentials + .create(opts) + .then(function (credential) { + resultEl.value = JSON.stringify({ + id: credential.id, + rawId: __oryWebAuthnBufferEncode(credential.rawId), + type: credential.type, + response: { + attestationObject: __oryWebAuthnBufferEncode( + credential.response.attestationObject, + ), + clientDataJSON: __oryWebAuthnBufferEncode( + credential.response.clientDataJSON, + ), + }, + }) + + resultEl.closest("form").submit() + }) + .catch((err) => { + console.error(err) + }) + } + + function __oryPasskeySettingsRegistration() { + const dataEl = document.getElementsByName("passkey_create_data")[0] + const resultEl = document.getElementsByName("passkey_settings_register")[0] + + if (!dataEl || !resultEl) { + console.debug( + "__oryPasskeySettingsRegistration: mandatory fields not found", + ) + return + } + + let opt = JSON.parse(dataEl.value) + + opt.publicKey.user.id = __oryWebAuthnBufferDecode(opt.publicKey.user.id) + opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge) + + if (opt.publicKey.excludeCredentials) { + opt.publicKey.excludeCredentials = opt.publicKey.excludeCredentials.map( + function (value) { + return { + ...value, + id: __oryWebAuthnBufferDecode(value.id), + } + }, + ) + } + + navigator.credentials + .create(opt) + .then(function (credential) { + resultEl.value = JSON.stringify({ + id: credential.id, + rawId: __oryWebAuthnBufferEncode(credential.rawId), + type: credential.type, + response: { + attestationObject: __oryWebAuthnBufferEncode( + credential.response.attestationObject, + ), + clientDataJSON: __oryWebAuthnBufferEncode( + credential.response.clientDataJSON, + ), + }, + }) + + resultEl.closest("form").submit() + }) + .catch((err) => { + console.error(err) + }) + } + + // Deprecated naming with underscores - kept for support with Ory Elements v0 + window.__oryWebAuthnLogin = __oryWebAuthnLogin + window.__oryWebAuthnRegistration = __oryWebAuthnRegistration + window.__oryPasskeySettingsRegistration = __oryPasskeySettingsRegistration + window.__oryPasskeyLogin = __oryPasskeyLogin + window.__oryPasskeyRegistration = __oryPasskeyRegistration + window.__oryPasskeyLoginAutocompleteInit = __oryPasskeyLoginAutocompleteInit + + // Current naming - use with Ory Elements v1 + window.oryWebAuthnLogin = __oryWebAuthnLogin + window.oryWebAuthnRegistration = __oryWebAuthnRegistration + window.oryPasskeySettingsRegistration = __oryPasskeySettingsRegistration + window.oryPasskeyLogin = __oryPasskeyLogin + window.oryPasskeyRegistration = __oryPasskeyRegistration + window.oryPasskeyLoginAutocompleteInit = __oryPasskeyLoginAutocompleteInit + + window.__oryWebAuthnInitialized = true +})() diff --git a/x/webauthnx/nodes.go b/x/webauthnx/nodes.go new file mode 100644 index 000000000000..309f49f80e9d --- /dev/null +++ b/x/webauthnx/nodes.go @@ -0,0 +1,87 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package webauthnx + +import ( + "crypto/sha512" + _ "embed" + "encoding/base64" + "fmt" + "net/url" + + "github.com/ory/kratos/x/webauthnx/js" + + "github.com/ory/x/stringsx" + "github.com/ory/x/urlx" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" +) + +func NewWebAuthnConnectionTrigger(options string) *node.Node { + return node.NewInputField(node.WebAuthnRegisterTrigger, "", node.WebAuthnGroup, + node.InputAttributeTypeButton, node.WithInputAttributes(func(a *node.InputAttributes) { + //nolint:staticcheck + a.OnClick = fmt.Sprintf("%s(%s)", js.WebAuthnTriggersWebAuthnRegistration, options) + a.OnClickTrigger = js.WebAuthnTriggersWebAuthnRegistration + a.FieldValue = options + })) +} + +func NewWebAuthnScript(base *url.URL) *node.Node { + src := urlx.AppendPaths(base, ScriptURL).String() + integrity := sha512.Sum512(jsOnLoad) + return node.NewScriptField( + node.WebAuthnScript, + src, + node.WebAuthnGroup, + fmt.Sprintf("sha512-%s", base64.StdEncoding.EncodeToString(integrity[:])), + ) +} + +func NewWebAuthnConnectionInput() *node.Node { + return node.NewInputField(node.WebAuthnRegister, "", node.WebAuthnGroup, + node.InputAttributeTypeHidden) +} + +func NewWebAuthnLoginTrigger(options string) *node.Node { + return node.NewInputField(node.WebAuthnLoginTrigger, "", node.WebAuthnGroup, + node.InputAttributeTypeButton, node.WithInputAttributes(func(a *node.InputAttributes) { + //nolint:staticcheck + a.OnClick = fmt.Sprintf("%s(%s)", js.WebAuthnTriggersWebAuthnLogin, options) + a.FieldValue = options + a.OnClickTrigger = js.WebAuthnTriggersWebAuthnLogin + })) +} + +func NewWebAuthnLoginInput() *node.Node { + return node.NewInputField(node.WebAuthnLogin, "", node.WebAuthnGroup, + node.InputAttributeTypeHidden) +} + +func NewWebAuthnConnectionName() *node.Node { + return node.NewInputField(node.WebAuthnRegisterDisplayName, "", node.WebAuthnGroup, node.InputAttributeTypeText). + WithMetaLabel(text.NewInfoSelfServiceRegisterWebAuthnDisplayName()) +} + +func NewWebAuthnUnlink(c *identity.CredentialWebAuthn, opts ...node.InputAttributesModifier) *node.Node { + return node.NewInputField( + node.WebAuthnRemove, + fmt.Sprintf("%x", c.ID), + node.WebAuthnGroup, + node.InputAttributeTypeSubmit, + opts..., + ).WithMetaLabel(text.NewInfoSelfServiceRemoveWebAuthn(stringsx.Coalesce(c.DisplayName, "unnamed"), c.AddedAt)) +} + +func NewPasskeyUnlink(c *identity.CredentialWebAuthn, opts ...node.InputAttributesModifier) *node.Node { + return node.NewInputField( + node.PasskeyRemove, + fmt.Sprintf("%x", c.ID), + node.PasskeyGroup, + node.InputAttributeTypeSubmit, + opts..., + ).WithMetaLabel(text.NewInfoSelfServiceRemovePasskey(stringsx.Coalesce(c.DisplayName, "unnamed"), c.AddedAt)) +} diff --git a/x/webauthnx/user.go b/x/webauthnx/user.go new file mode 100644 index 000000000000..bc369f6851ab --- /dev/null +++ b/x/webauthnx/user.go @@ -0,0 +1,47 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package webauthnx + +import ( + "github.com/go-webauthn/webauthn/webauthn" + + "github.com/ory/x/stringsx" +) + +var _ webauthn.User = (*User)(nil) + +type User struct { + Name string + ID []byte + Credentials []webauthn.Credential + Config *webauthn.Config +} + +func NewUser(id []byte, credentials []webauthn.Credential, config *webauthn.Config) *User { + return &User{ + ID: id, + Credentials: credentials, + Config: config, + } +} + +func (u *User) WebAuthnID() []byte { + return u.ID +} + +func (u *User) WebAuthnName() string { + return stringsx.Coalesce(u.Name, u.Config.RPDisplayName) +} + +func (u *User) WebAuthnDisplayName() string { + return stringsx.Coalesce(u.Name, u.Config.RPDisplayName) +} + +func (u *User) WebAuthnIcon() string { + return "" // Icon option has been removed due to security considerations. +} + +func (u *User) WebAuthnCredentials() []webauthn.Credential { + return u.Credentials +}